Unreal Engine 5 C++ The Ultimate Game Developer Course 1-16
カメラ操作
左クリック + WASDでできて便利
Gキーでゲームビューにできて、アイコンを消せる。
F11で全画面にできるので、キャプチャがほしいときなどにも役立つ。
SSを取ることもできる
座標
z軸が真上。画面左下のギズモは、ワールド座標
物理演算
選択 > Physics > Simulate Physicsなどで有効化できる
2
Quixel Bridge
いろんなオブジェクトを、即座にインポートしたりできる
講義時点では無料だったが、現在は有料になってしまった。
アセットの持ち込み
持ち込みたいアセットのあるプロジェクトのフォルダで、右クリック→Migrate
https://scrapbox.io/files/69aba722f1c53c17322849da.png
OKを押して、保存先を持ち込み先のプロジェクトのContentフォルダなどにいれてやるとよい。
https://scrapbox.io/files/69aba72af1c53c17322849e3.png
データについて
青線: static mesh アセット
緑線: マテリアルを表す
赤線: マテリアルに適用するテクスチャたち
https://scrapbox.io/files/69aba867f1c53c1732284b27.png
オブジェクト操作知識関連
選択してendキーで、オブジェクトをぴったり机などに置くことができる
オブジェクトをgizmo経由で動かすとき、shiftをおすとカメラも移動する
オープンワールド
従来はマップ1を読み込み、端に行くとマップ2を読み込むという仕組みなので、実装が大変だった。UEは1つの大きなマップをパーティション分割して読み込む機能を持つ。
作る
File > New Level から、オープンワールドのテンプレートファイルなどを呼べる。
レベルの保存は、Maps というファイル内で行うことにする。保存するときはSave Current Level as...からMapsを指定して保存する。
https://scrapbox.io/files/69abe4f6f1c53c1732289417.png
空と大気
Sky Atomoshere
Directional Light(指向性ライト: 影などが同じ角度に落ちるようにするというイメージ)
Sky light
Exponential Height Fog
Volumetric Clouds
マップを増やしたら、Project Settingsでデフォルトで開くマップを設定できる
https://scrapbox.io/files/69abe735f1c53c1732289647.png
空と大気を用意してみる
□+みたいなアイコンから、Place Actors Panel で一般的にレベルに配置するアクターと呼ばれる要素を表示できる。direction、skyなどで検索して大気と指向性ライトを追加すると空が出る。
ctrl L で、ctrlを押しながらマウスで太陽の場所などを操作できる
ctrl shift l で、ctrl shiftを押しっぱなしでマウスドラッグだけで操作できる
Mobility
static: ゲーム内で光の位置や方向が変わらない。高速で動作する。
stationary: ゲーム内で光の強度などを変えられるが、移動などはできない
Movable: 太陽が実際に動いて、動的な影の描写などができるが重い。
Sky Light(天窓)
環境光などを反映する仕組み。山の緑とかそういうの。
sky light で検索して設置できる。moveable設定にして、real time captureにチェックすると、動的に反映されるようになる
Exponental Height Fog
シーンに霧などを追加して遠近感を表現するためのもの。遠くがかすむ感じとか。指数関数的に、高い場所に行けば行くほど濃くなるような設計が基本組まれている
volumetric clouds
たとえば2Dの雲はskyboxでつくった(3d tdでつくった)が、3Dになると、密度や厚みを表現した雲を用意できる
背景設定
https://scrapbox.io/files/69abf511f1c53c173228a6dd.png
Landscapeモードを使って背景を作ってみる
生成後四角いフレームになることがある。world partitionで、指定範囲を囲んでloadさせるとうまく出る
https://scrapbox.io/files/69abfb37f1c53c173228ac99.png
Landscapeを消すときは通常のSelectionモードに戻ってから削除する。また消せないときは、子要素から少しずつ削って消していくとよい
広すぎる場合はオブジェクトを選んで、Fを押すとそのオブジェクトにカメラが近づく
Sculpt(彫刻)機能
LandscapeのMaterialを作る
マテリアルを右クリックで作成し、開く
Fully roughにチェックを入れて、光沢などが出ないようにする
⭐テクスチャが使えないので、フリーアセットで落としたものでレイヤーブレンドを学ぶ。
alt クリックで取りけし
そもそもテクスチャのことについて良く知らない。テクスチャは単純に画像ファイルのこと。
https://scrapbox.io/files/69af88bc6d5dc11eb502511a.png
ベースカラー(Diffuse)
純粋な色データを指す。光の描写などは含めていない。
法線マップ(Normal)
表面の細かい凹凸(砂の波紋など)を表現するためのデータ
光がどのように反射するかをRGBの色情報に変換して保存されている。ポリゴンで表現すると非常に負荷がかかるので、このデータを使って凹凸をポリゴン無しに表現させられる。
パックドテクスチャ(ORDp)
メモリを節約するために異なる白黒の画像を1舞のRGBチャンネルにパックしたデータ。
LandscapeモードでTarget Layerを設定する
https://scrapbox.io/files/69b0ffe36d5dc11eb504c119.png
UE5.5では、このボタンを押さないと表示されない。調整できる
アセットストアで無料のものを探す
気になったものをライブラリに入れておく。中品質とかでいったん落とす。
アセットパックがいい(GLTFは、Blenderなどの形式向け)
https://scrapbox.io/files/69b105fa6d5dc11eb504cea8.png
UE5に導入
Window -> Fabで、UEの操作でFabを開く。
+で導入すればOK(UEから入れるとうまくいくことが多い)
Foliage Painting
いろんな背景を追加してみる。補足)UEのMarket Placeが、今でいうFab
Static MeshのオブジェクトをFoliageとすることで、背景で使うような素材を軽いものにすることができる。
緑のものがFoliage meshの素材
https://scrapbox.io/files/69b131496d5dc11eb50534c4.png
通常のstatic mesh素材しかないマテリアルも、こちらに加えることで、Foliageとして保存して管理することができるようになる。(水色が通常で、緑色が作ったもの)
https://scrapbox.io/files/69b1318c6d5dc11eb5053522.png
Foliageを指定した後、PaintでそのFoliageをベースにした形でマップを塗り進めることができる
shift おしてpaintで消しゴムとして扱える
重くなってきたら、レンダリングの設定を行う。
https://scrapbox.io/files/69b138d86d5dc11eb505401f.png
アンチエイリアスなども調整。
ゲームビューにすると、設置した木に衝突しないことがわかるので、コリジョンを設定してやる
https://scrapbox.io/files/69b13b616d5dc11eb5054461.png
https://scrapbox.io/files/69b13b296d5dc11eb505440c.png
終わった後に塗り直さないといけないかも。
Post Process Volume
マップに置くことで、フィールドの雰囲気を柔軟に調整することができるようになる。気になったら調べてみるとよいだろう
Packed Lavel actors
世界の壁を作る。取得したアセットを貼り付けていくことになる。ある程度のまとまりをつくったら全選択 > level > create packed level actor で、複数オブジェクトを1つにまとめて軽くするような調整ができる。
https://scrapbox.io/files/69b20e116d5dc11eb5062074.png
You can not create an asset named 'BPP_CanyonWall1' because there is already a map file with this name in this folder.
同名でLevelファイル、BPを作ろうとしたときのエラー。この操作はBlueprintと、Level / Mapファイル(まとめたオブジェクトの配置情報データ)を作っている。画面上でBPのファイルは消したが、セットで作ったLevelマップが残っている。消した後も、下記でリダイレクタをリフレッシュする必要があるので覚えておく。
https://scrapbox.io/files/69b211326d5dc11eb50628d8.png
必要ならEpic側の用意するサンプルプロジェクトから、引っ張ってくることもできる(Migrateでデータを引っ張ってくるあの手法を使う)
今回はMASSフォルダを選択してMigrate > Slash5.5のContentsフォルダに入れて持ってくることにした。
重い
Waiting on static mesh StaticMesh xxx being ready before playing...
Level Instance
複数アクターを一つのレベルとしてまとめ、アセットとして再利用できる機能のこと。Prefabなどと考え方は同じ。
右クリックで空のレベルインスタンスを作成。detailsから、レベルインスタンスを選んで選択すると、その情報を反映できる。空の情報なども含めて。
これを使うとダンジョンのアセットなどをFabで落として、それをレベルインスタンスとして使って簡易にダンジョンなどを素材のまま表現できる
ベクトル基礎
https://scrapbox.io/files/69b677aab9e150750ae2e944.png
このベクトルの成分は、(20 - 5, 20 - 5) = (15, 15)で表せる。
code:txt
v1 = (1, 2) v2 = (3, 2)のとき
v1 + v2 = (1 + 3, 2 + 2) = (4, 4)
v1 - v2 = (1 - 3, 2 - 2) = (-2, 0)
練習
code:txt
(2, 2, 0)の位置から、(3, 0, 2)のベクトルでロケットランチャーを打った。着弾点は?
(5,2,2)
⭐よく考えるとキャラの座標と、ベクトルを足した結果なので、座標と結果を足したことになっている
ここで使う考え方が「位置ベクトル」で、座標を位置ベクトルと表現することで、ベクトル同士の加減を実現している
(全ての点を、原点からの矢印として扱うということ。エンジン内部では空間内のobjectの位置をすべてベクトルで表す。)
Magnitude (大きさ)
ベクトルの大きさのこと。敵と自分がどの程度離れているかなどを表現する
直角三角形で表すことができるのでx^2 + x^2 = r^2が成り立つ
つまりx = 3, y = 4のベクトルの大きさrは、 9 + 16 = 25, r = ±5。これは三角形の辺の長さであるためマイナスは考慮されない。よってr = 5
|v| = √(x^2 + y^2)
使用時のイメージ
キャラが長さ10の釣り竿を持っている。魚がヒットしたとき、キャラと魚の距離が10よりも離れたら何かを起こす
→これは、キャラと魚の大きさが10を超えたときどうするか、という処理を挟んでやればよい
正規化(normalized)
ベクトルの大きさが1のもののこと
ベクトルの大きさ20で移動する敵、とかそういうのをつくってもいいが、大きさを1として向きだけ分かるようにして、別ステータスでspeed = 20 として、移動時、20 * 単位ベクトル とかで設計したほうが綺麗。
code:式
1 / |V| * Vw = Vn (単位ベクトル)
大きさ20のベクトルなら、 1/ 20 * 20 = 1 の大きさになるようにする
座標系
https://scrapbox.io/files/69b68513b9e150750ae2fa13.png
UEはLeft Hand型
C++
Visual StudioでC++を動かす。
https://scrapbox.io/files/69b77667b9e150750ae43915.png
色々UE5が推奨する設定があるので、そちらに従い設計していく。
Reflection
実行時にプログラムを検査する機能。C++には標準で未搭載だが、UEには存在する。
chapter5
? ⭐ ctl f5 で、visual studioからコンパイル->UE5を起動することができるので、ファイル作成時などはこちらで対応する️
Actor Creation
C++クラスを作ってみよう。Tools > New C++ Class
作ったあとVisual Studioを開く。ファイルを始めて作成する場合は、UE5を閉じたあとにVisual Studioでビルドを掛けるのが確実。これでC++ Classesというディレクトリが作られる。
作られるファイル
#pragma once: コンパイル時に複数読み込まれる挙動を防ぐ。
C++クラスでブループリントを作る
Itemクラスを親として、子要素でブループリントを用意する。
右クリック > Blueprint Class > で、作りたいC++のクラスベースを選択して作る。
https://scrapbox.io/files/69bc0ddeb9e150750aeb6d24.png
Blueprintから作ったおかげで、このクラスをゲーム内に出したときにGizmoが表示されるようになり、多少操作しやすくなる
Blueprint
UE5においては、2つの側面を持つ。
ビジュアルスクリプティングとしてのBlueprint
文字でC++のコードを実装する代わりに、ノードと言われるブロックを線で繋いでゲームの処理を作る機能を指す
C++クラスをベースにして作ったBlueprintは、C++のプログラムの性質を引き継ぎ、UE5の編集画面上で扱いやすくしたものとして使われる。プログラマが移動処理を書いて、デザイナーが速度を100と決める、など。
アセットの設計図としてのBlueprint
3Dモデルや音、光やスクリプトなども含め、1つの箱にまとめて再利用可能にしたもの(Unityで言うPrefab)。
背景などの配置を軽量化するためのBlueprint Packed Level Actor として使われる。
ビジュアルスクリプティングとしてのBlueprintについて
Construction Script
ゲームが始まる前に呼ばれるBPのデータ。詳細には、そのBPのスクリプトのパラメータが変更されたとき呼ばれる。
Event Graphから設定したプリント関数などが走らないとき、ゲーム内で読み込まれていないからという理由のケースがある。ただし、C++側に書いたときはどうだろうか。
C++でログを出す
code:cpp
void AItem::BeginPlay()
{
Super::BeginPlay();
UE_LOG(LogTemp, Warning, TEXT("Begin Play Call C++!"))
}
終わったら、VIsual Studio側で保存し、UE5側でホットリロードすると早い。
https://scrapbox.io/files/69bcd7bdb9e150750aec233d.png
BPで実装した内容と、同じ内容をC++で書く
https://scrapbox.io/files/69bce07eb9e150750aec2eac.png
画面にHelloと出す
code:cpp
void AItem::BeginPlay()
{
Super::BeginPlay();
if (GEngine) // != nullptr
{
// key, 時間, 色, 出す文字列
GEngine->AddOnScreenDebugMessage(1, 60.f, FColor::Cyan, FString("Item OnScreen Message!"));
}
}
こういったように、スクリプトで実装できる内容をエディタだけで書けるので、UE5のBPはプログラミングできなくても大丈夫という謳い文句になっている
補足:GEngine
UEの核機能。ポインタとして扱われるのでnullptrチェックをしてエラーにならないかどうかをチェックするのが作法。(nullチェックしなくてもコンパイル自体は通る)
簡単なログを、FStringで出す
code:cpp
// このクラスで命名されたBPの名前を取得
FString Name = GetName();
FString Message = FString::Printf(TEXT("Item Name: %s"), *Name);
FStringは、UE独自の文字列クラス。文字列といえば標準はstd::stringだが、UEはPC, コンソール, スマートフォンなど様々なプラットフォームで動作する。当然文字コードの扱いもプラットフォームによって異なるという理由から、それらを吸収するために独自の文字型(TCHAR)及び、クラス(FString)を保持する仕組みになっている。
引数として渡されている、*Nameについて
Printfの%sは純粋な文字しか受け付けない。Nameのまま渡すと、FStringクラスのデータを渡すことになるので加工しなくてはならない。なので、UEのFStringクラスで、*Nameとすると、生の文字データを取り出せるように設計している
詳しくは、演算子のオーバーロード機能という形で調べてみると良い
code:cpp
class FString {
const TCHAR* operator*() const {
return 内部の生の文字配列データ;
}
Debug Sphereを作る
https://scrapbox.io/files/69bd047ab9e150750aec5d32.png
LocationはそのActorの場所で、Sphereを描画するBP
C++で書くと、以下のようになる
code:cpp
UWorld* World = GetWorld();
if (World)
{
FVector Location = GetActorLocation();
DrawDebugSphere(World, Location, 25.f, 24, FColor::Red, false, 30.f);
}
DrawDebugLine
https://scrapbox.io/files/69bd0d47b9e150750aec6950.png
やっていることとしては、始点をselfの座標、終点をself + GetActorForwardVectorの座標としている。ただ、GetActorForwardVectorは正規化ベクトルなので、そのまま使うと長さ1である。なので、途中にmultiplierを挟むことで100倍して、実質長さ100の線を書き込むという形になっている
c++で書くと以下。
code:cpp
UWorld* World = GetWorld();
FVector Location = GetActorLocation();
if (World)
{
FVector Forward = GetActorForwardVector();
// lifetime = -1 とすると、永続扱い
DrawDebugLine(World, Location, Location + Forward * 100.f, FColor::Red, true, -1.f, 0, 1.f);
}
Chapter6
AddActorWorldOffset
割り当てたActorの位置を指定の値分ずらす
補足として、BlueprintとC++スクリプトの実行順序は、Blueprint側で指定した動きが先に動く。BP側でAddActorWorldOffsetを設定したとしても、C++側でSetActorLocation()などで位置を調整していると、変化しないように見えるので注意。
code:cpp
void AItem::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
AddActorWorldOffset(FVector(1.f, 0.f, 0.f));
DRAW_SPHERE_SingleFrame(GetActorLocation());
}
これで毎フレーム、x軸に+1だけ動くようなActorが作れた。ただしこれはFPSに依存して速度が変わるという問題があるので、当然対応する必要がある。DeltaTimeを使えば良い。
code:cpp
// 50(cm/s) と定義する
float MovementRate = 50.f;
// 距離 = 速さ * 時間 で、DeltaTime秒中に進む距離を求めれば良い。
// MovementRate(cm/s) * DeltaTime (s/frame) = cm/s * s/frame = cm/frame
// フレームあたりに進むcmとなる
AddActorWorldOffset(FVector(MovementRate * DeltaTime, 0.f, 0.f));
DRAW_SPHERE_SingleFrame(GetActorLocation());
Unityでも散々やってきたが、単位で考えるのが非常にわかりやすいかも。
三角関数を使う
code:txt
sinθ と cosθ の範囲は、 -1 ~ +1 の間
値をt, sin()をブラックボックスとして考えると、
t -> sin(t) -> sin(t)
横軸をゲーム開始からの時間tと捉えると、θ = t としたとき、
sin(t)が常に-1 ~ +1 の値を返してくれるようになる
https://scrapbox.io/files/69bf8db9b9e150750aeea077.png
+Addもしくは、VARIABLES(変数)の欄から、float型のTを作成
Tをゲッタ(ctrlでドラッグ) と、セッタ(Altでドラッグ)で作る。ゲッタの先にはAddをつなげて、 T = T + DeltaTimeで蓄積されるようにする。
今回の場合は、ActorのZ軸(右クリックで詳細にLocateを区割りできる)に、
Tの10倍の値を
sin(T)として渡して
-1 ~ +1 で変える値を 0.5倍して、 z軸が-0.5 ~ +0.5 だけ動くようにしている
変数の公開
Unityでいう、[SerializeField]の形で公開できる
code:cpp
private:
UPROPERTY(VisibleInstanceOnly)
float RunningTime;
UPROPERTY(EditDefaultsOnly)
float Amplitude = .25f; // 振幅
UPROPERTY(EditInstanceOnly)
float TimeConstant = 5.f; // RunningTimeにかける値 2π / k における、K
EditDefaultsOnlyなら、BP内部の編集が、EditInstanceOnlyならOutliner上で変数の値を維持れるようになる。
EditAnyWhereであれば、どちらでも編集が可能となる。
また、VisibleInstanceOnlyのように、経過時間だけを見て編集だけしたくない場合はこのように書けば公開のみ(編集不可)とできる。
UPROPERTY()について
OutlinerやBP上のDetailsタブに公開するためのマクロ。
イベントグラフ上で使いたい場合は、別途公開設定をする必要がある。
code:cpp
protected:
virtual void BeginPlay() override;
// protectedに移さないと、公開できない
UPROPERTY(EditAnywhere, BlueprintReadOnly)
float Amplitude = .25f; // 振幅
private:
// ...
https://scrapbox.io/files/69bf9cd8b9e150750aeeb256.png
セッターならBlueprintReadWriteという感じ。継承した変数を出したいときは設定で変更する。
https://scrapbox.io/files/69bf9d7cb9e150750aeeb309.png
パラメータ
code:Item.h
protected:
virtual void BeginPlay() override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Sine Parameters")
float Amplitude = .25f; // 振幅
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Sine Parameters")
float TimeConstant = 5.f; // RunningTimeにかける値 2π / k における、K
項目名を設定できる
https://scrapbox.io/files/69bfa199b9e150750aeeb862.png
private変数だが、Blueprintイベントグラフで閲覧だけしたい
code:cpp
private:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
float RunningTime;
使えるようになる。
https://scrapbox.io/files/69bfa29fb9e150750aeeb9a6.png
Blueprint Event Graphで関数を公開する
まず基礎として、関数の定義方法
code:Item.h
protected:
UFUNCTION(BlueprintCallable) // BP EventGraph対応
float TransformedSin(float Value); // 追加
code:Item.cpp
float AItem::TransformedSin(float Value)
{
return Amplitude * FMath::Sin(Value * TimeConstant);
}
https://scrapbox.io/files/69bfa515b9e150750aeebc31.png
このような形で使えるようになる
UFUNCTION(BlueprintPure)
BP上のグラフを、Pure(緑色)の状態で設定することができる
Pure関数はinput / exec のピン▷がなく(入力 or 実行)、指定の値だけを返すことができる。厳格に管理する場合はこちらを使って管理するのが好ましい
https://scrapbox.io/files/69bfa662b9e150750aeebe57.png
テンプレート関数
UEにおけるComponent
UEの世界(Levelと呼ぶ)に配置できるオブジェクトをActorと呼ぶ。ただしこれ自体は見た目や機能なども持っていない。そちらに追加していくのが Componentと呼ばれるものとなる。見た目、音、衝突判定など。
WeaponというActorがあるとする。例えば斧なら斧の形をしたMesh Componentを持つ必要がある。斧を振るときの判定を作りたければ刃の部分にBox Componentをつけて判定を調整する必要がある。
Attachment(階層構造)
1つのActorの中に複数のComponentを付与していくことになるが、それらを親子関係でつなぐこと。
例えば親ComponentのLocationが移動したとき、アタッチされた子要素も一緒に動くという形。
DefaultSceneRoot
Blueprintなどで空のActorを新規作成すると付与されているComponent
Actor自体は3D空間上の位置、回転、大きさの情報を持っていない。なのでActorをとりあえず3D空間に設置するにはTransformを持った物体でなければならないので、デフォルトでこれを持たせている
あくまで仮の箱なので、Static Meshコンポーネントなどを追加してDefaultSceneRootに重ねることで差し替えると、役目を終えて消滅する。
C++スクリプトでComponentを割り当てる
ItemMeshポインタの作成
code:Item.h
private:
UPROPERTY(VisibleAnywhere)
UStaticMeshComponent* ItemMesh;
};
C++の一般的な考え方として、スタックとヒープがあったはず。今回持たせようとしているStatic Meshは巨大で複雑なデータのためヒープ領域に作るべき。また、UE5ではUObjectから派生するすべてのオブジェクトはユーザーが勝手に作成・破棄してはいけないというルールが存在する。理由としては、UE5にはガベージコレクションが導入されているのでそちらで勝手に管理するから。
UPROPERTY
BlueprintでDetailタブに公開するためのマクロでもあるが、今回はUE5にこのコンポーネントをGC管理対象とするために割り当てている。GCが管理するのは、UPROPERTY()の付与された変数だけ。なのでつけ忘れると勝手にメモリから消去されてしまうことがあるのでバグの原因になる。
では自分で管理すれば付与しなくていいのかというと、それもNO(ルールに接触するので)。
Mesh Componentの割当
code:Item.cpp
// コンストラクタ
ItemMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("ItemMeshComponent")); // Component作成
RootComponent = ItemMesh; // 作ったコンポーネントをルートコンポーネントとして、DefaultSceneRootから置き換える
CreateDefaultSubobject
通常C++では、newを使ってオブジェクトを作るが、UE5で勝手に作成 / 破棄してはいけないので、UE5の用意する作成方法でオブジェクトを作る。それがこれ。
UStaticMeshComponentを、TEXT("ItemMeshComponent")で作る。
RootComponent
class SLASH_API AItem : public AActorと、親であるAActorで定義されているもの
? ポインタ* とするかは、どう判断するか
UE5であれば、名前のプレフィックスで判断すると良い。
UObjectやAACtorなどから派生するクラス(U, A)はポインタ
UStaticMeshComponent, UBoxComponent, AItem
FVector(F: 構造体(struct)であることを示す) や基本的な型はスタック扱い
FString, FRotator, int32, float...
アイテムを回してみる
x: 赤, 緑: y, 青:z
https://scrapbox.io/files/69c0b201b9e150750aeffd50.png
AddActorWorldRotationかAddActorLocalRotationで実現する。ワールド座標で見るか、ローカル座標で見るか。
code:cpp
AddActorLocalRotation(FRotator(.0f, 1.0f, .0f));
これで、z軸回転を実現できる。yの値を変えているように見えるが、FRotatorはPitch(y), Yaw(z), Roll(z)を引数として受け取るのでこれで合ってる。
DeltaTimeを考慮させるなら下記のような形で実現できる
code:cpp
float RotationSpeed = 45.0f; // deg/s : 1秒あたり45度 float DeltaRotation = RotationSpeed * DeltaTime; // deg/s * s = deg : 1フレーム(delta)あたりの度数 AddActorLocalRotation(FRotator(.0f, DeltaRotation, .0f));
Chapter7
新しいクラスを、Pawnを土台として作る
Tools > New C++ Class > Pawn
https://scrapbox.io/files/69c0f273b9e150750af0984c.png
Pawnを親とすることで、プレイヤーの入力を受け付けるようなActorを作れる。
鳥を作ろう
Capsule Componentで衝突処理を作る。メッシュポリゴンで行うと負荷が大きくなるので、簡易的な判定を用意する。
code:cpp
#include "Components/CapsuleComponent.h" private:
UCapsuleComponent* Capsule;
⭐️includeするファイルは、ドキュメントを参照しよう
#include "Bird.generated.h"は必ず末尾二定義する
2>C:\Users\watas\Ue5Project\Slash 5.5\Source\Slash\Public\Pawns\Bird.h(1): error : #include found after .generated.h file - the .generated.h file should always be the last #include in a header Forward Declaration
Skeletal Meshとアニメーション
骨格メッシュ。Actorに付与することでアニメーションに対応する。ピンク色。
ボーンにメッシュを付与していくことがリギング。
https://scrapbox.io/files/69c10463b9e150750af0ba22.png
Animation
ボーンをどう動かすかを含んだファイルのこと。緑色。
https://scrapbox.io/files/69c105a5b9e150750af0bc45.png
code:cpp
ABird::ABird()
{
Capsule = CreateDefaultSubobject<UCapsuleComponent>(TEXT("Capsule"));
SetRootComponent(Capsule);
BirdMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("BirdMesh"));
BirdMesh->SetupAttachment(GetRootComponent());
}
今回、C++側でCapsuleComponentをrootとして割当て、SkeletalMeshComponentを子要素に入れた。そのあとBlueprint側で、どのSkeleton Meshを割り当てるのか、アニメーションはどうするのかを設定。
Input対応を行う
Shift + f1でゲーム画面から戻ってOutlinerを操作できる。
Pawnクラスの持つAuto Possess Playerの設定で、Playerに対応するコントローラーの設定ができる。例えば下記でPlayer 0 を選択すると自分が操作する形で割り当てられ、Birdを操作できる状態となる(ただし、Scriptで入力を受け付けるコードを書く)。
https://scrapbox.io/files/69c1e9d69ea0767ac850ba3f.png
マッピング設定
https://scrapbox.io/files/69c1eb2b9ea0767ac850be4a.png
今回はAxis Mappingで。なお記載の通り、Enhances Inputが後継のInput処理なので、そちらも後で。
複雑なのでメモより講義を見返す。
カメラの調整
Birdのカメラを三人称視点とする
rootコンポーネントを選んで+Add で、Cameraを追加する
https://scrapbox.io/files/69c1f7549ea0767ac850e46c.png
正面の設定が合わない
UE5ではx軸のプラスがForwardとして扱われる。対して3Dモデルの正面はy軸を正面として作られることが多い。なので、カラスのメッシュを回転させてUE5の正面と3Dモデルの正面を一致させるように設計してやる。カメラをMeshの子要素で配置すると一緒に回転するので注意。
Spring ArmでMeshとカメラを連結する
カメラを単純に後ろに配置するのではなく、Sprimg Armで連結すると良い。例えば壁際のカメラ処理などを、伸び縮みして対応してくれるようになる
https://scrapbox.io/files/69c1fb9a9ea0767ac850f18e.png
回転処理
Controllerの仕組み
ゲームプレイするとすぐにデフォルトで生成される。そのときにPawn側で設定した、最初のクラスにそのコントローラを割り当てている。
回転処理を加える。Project SettingからInputで新しいバインドを作る
Mouse Xのスケールを取るようにする。
PIEモードでコンソールを開く
海外キーボードでは~, 日本語では@キー
https://scrapbox.io/files/69c373539ea0767ac8533655.png
show collision等で判定が可視化するコマンドなどがある
⭐Rotatorの軸についてまとめ
Roll
x軸。横転。飛行機が翼を左右に傾けるイメージ。
地面を転がるなど。
Pitch
y軸。仰俯角(水平を基準とした上下方向)。機首を上下に振る、お辞儀のような動きがイメージ。
FPSで上下を向く動き。
Yaw
z軸。旋回。水平方向を左右に向く、首を振る動き。
FPSで左右を向く動きをする場合、この軸を使う。
UE5と他のツールとの上方向の際
UE5, Blender: z軸が上。
Unity, Maya: y軸が上。
マウスでYawを操作する例
https://scrapbox.io/files/69c3752e9ea0767ac85339c6.png
InputAxis Turnでマウス左右入力の検知。右が+, 左が-
ちなみにTurnというのは、Project SettingsでAxisマッピングで設定したところで自分で命名したもの
Add Controller Yaw InputでYawに回転の追加。UEのプレイヤーコントローラの持つ回転値に、マウスの移動量を加算する。
DetailタブでUse Controller Rotation Yawにチェックを入れておくと、コントローラが右を向いたとき、Pawn事態も同じ方向を向かせるという設定を入れれば、振り向いてくれる。
操作するPawnのCollisionを設定する
現状地面などをすり抜けるが、それはBlueprintのCollision > Collision Presetで設定できる。
Default Pawn
https://scrapbox.io/files/69c37cee9ea0767ac8534d43.png
いつの間にか生成されている、この球のこと。ゲームモードの設定で、デフォルト設定になっているとこのPawnが生成されるような設計になっているので産まれている。防ぎたい場合は、自分で独自のゲームモードを作ってやればよい。
右クリック > Blueprint Class > Game Mode Baseで作成。
Blueprintを開いて、Default Pawn Classに、作った鳥を設定。
https://scrapbox.io/files/69c37e299ea0767ac8534fbc.png
World Settingsで、GameMode Overrideで上書き。
https://scrapbox.io/files/69c37e3a9ea0767ac8534ff7.png
これで、Worldに鳥を設置していなくても、起動した瞬間に鳥が生まれるようになる。
Spawn位置の決定
Place Actors Panel > Player StartをWorldに配置すると、そこが初期位置となる。
https://scrapbox.io/files/69c380889ea0767ac8535453.png
Chapter8
Character Classを作る
Valley of the Ancientの掃除
https://scrapbox.io/files/69c384759ea0767ac8535c45.png
code:txt
While trying to load package /Game/AncientContent/Audio/AudioModulation/ControlBuses/Echo_ControlBus, a dependent package /AudioModulation/Volume was not available. Additional explanatory information follows:
FPackageName: Unable to identify a valid mount point associated with skipped package /AudioModulation/Volume. The package root is unknown.
https://scrapbox.io/files/69c384cc9ea0767ac8535cdc.png
求められているプラグインを有効かしてから、消すことを試すと消せる。
SlashCharacterを作る
C++クラスをPublicフォルダで、右クリック > New C++ Classで作成。Visual Studioを立ち上げると、警告が出るのでReload Allで良い。Live Codingが終わったらコード整理。
Blueprint Classで作成 -> ここで先ほど作ったCharacterをベースにしたC++スクリプト選択。
Characterに必要なComponentがデフォルトで付与されている。UE側でidleアニメ等も用意されているので、それを使えば待機モーション等もすぐ実装できる
アクセスマッピングの調整
Birdと同じように、マウス操作で軸を変える。ただし人間だと、マウスを上下左右に傾けたときに回転させるのではなく、カメラだけ動くような調整が必要
code:cpp
ASlashCharacter::ASlashCharacter()
{
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;
https://scrapbox.io/files/69c49bd09ea0767ac854ab10.png
このあたりの設定を、C++側で無効化する
Spring Armの設定で、Use Pawn Controller Rotationをtrueにする。これは操作キャラクター(Pawn)の向きを、プレイヤーの視点(Controller)の向きに強制的に合わせるかどうか。これをtrueにして、Mesh側でControllerのRotationを使わないようにすることで、カメラだけが移動するような挙動を実現できる
キャラの移動方向の調整
既存
https://scrapbox.io/files/69c49e299ea0767ac854b6d3.png
code:cpp
void ASlashCharacter::MoveForward(float Value)
{
if (Controller && (Value != 0.f))
{
FVector Forward = GetActorForwardVector();
AddMovementInput(Forward, Value);
}
}
この状況でWを押すと、キャラの向いている前方向に移動する。ただしゲーム的にはカメラから見て↑に進みたい。
Rotation Matrix(回転行列)
https://scrapbox.io/files/69c4a7459ea0767ac854cad4.jpeg
キャラの方向ではなく、Controller(カメラ)のほうに動かす
code:cpp
# 現在
FVector Forward = GetActorForwardVector();
AddMovementInput(Forward, Value);
# 変更後
const FRotator ControlRotation = GetControlRotation();
const FRotator YawRotation(0.f, ControlRotation.Yaw, 0.f);
const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
AddMovementInput(Direction, Value);
Controllerの回転情報を取得して、Yawを取得。
回転行列にFrotatorのYawRotationを変換して、関数でX軸の単位ベクトルを取得してFVectorのDirectionに入れる。
その他
Orient Rotation to Movementにチェックを入れると、移動方向に回転してくれるようになる
https://scrapbox.io/files/69c4eed09ea0767ac8557c4b.png
キャラの髪の毛などの調整
素材からEchoだけ引っ張ってくる(全部は重いので)
CharacterでMigrateを選択。SlashプロジェクトのContentに格納すればOK
Groomとして、髪の毛等のアセットがある
Groom(グルーム)とは、髪の毛や動物の毛(ファー)を、リアルに描画、物理シミュレーションするための専用データ。旧式のデータとして髪の毛テクスチャを張り付ける、ヘアガードと呼ばれる手法があったが、それよりもデータを持たせやすくしたもの。
Groom Componentを付与することで、Skeleton Meshの頭に追従するように設定する
Name UGroomComponent
Type class
Header File /Engine/Plugins/Runtime/HairStrands/Source/HairStrandsCore/Public/GroomComponent.h
Include Path #include "GroomComponent.h" たとえばSpringArmなど、今までは
Header File /Engine/Source/Runtime/Engine/Classes/GameFramework/SpringArmComponent.h
と、Engine配下だったものを使い続けてきた。ただ今回はHairStrandsなどの配下にある。これらはUE5の標準モジュールだったのだが、Groomは異なる。なので、これらを使うことを宣言してやる必要がある。
宣言
code:Slash.Build.cs
// ...
PublicDependencyModuleNames.AddRange(new string[] {
"Core", "CoreUObject", "Engine", "InputCore" , "HairStrandsCore" // <-追加
});
終わったら一度コンパイル。UE側のデータをexplorerで開く。
下記当たりのデータは、コンパイルや再度開いたときに自動で生成されるので一度消してしまってもよい。
https://scrapbox.io/files/69c4f8b59ea0767ac8559613.png
.upprojectでその他の操作 -> Generate Visual Studio project filesで作り直す。
https://scrapbox.io/files/69c4f9109ea0767ac85596e8.png
終わったら、upprojectをダブルクリックしてリビルドを試す。結構怖い
UE5が開いてデフォルトの画面が出る。
Slash.slnをvisual studioで開く。
Component割り当て
code:cpp
// Hairという名前でGroomコンポーネント付与 Meshの子要素化、headソケットに付与
Hair = CreateDefaultSubobject<UGroomComponent>(TEXT("Hair"));
Hair->SetupAttachment(GetMesh());
Hair->AttachmentName = FString("head");
Eyebrows = CreateDefaultSubobject<UGroomComponent>(TEXT("Eyebrows"));
Eyebrows->SetupAttachment(GetMesh());
Eyebrows->AttachmentName = FString("head");
ソケット
3DキャラのMeshは1つの塊ではなく、階層構造を持っている。今回、Meshにアタッチするだけでは足元などルート座標にくっついてしまうので、Meshの中のhead(頭骨)部分に追従するようなスクリプトを指定している
Chapter9
Animation Blueprint
ジャンプ、移動アニメを付与する。Skeleton Meshにはアニメーションを付与できるが、今回Assetではなく、Animation Blueprintを使用してアニメをつける。
https://scrapbox.io/files/69c547a59ea0767ac855fd4d.png
こちらで、取得したデータに応じたアニメーションの設定が実現できる。
設定イメージ
https://scrapbox.io/files/69c6031d9ea0767ac8572182.png
Event Blueprint Initialize Animation
このABPが生成されたとき(ゲーム開始時など)に一度だけ呼ばれる処理。ABPを所持している親(Pawn)を取得し、BP_SlashCharacterであるかをチェック。問題なければキャスト。Character Movementコンポーネントを取得して、ABP上で定義したMovement Componentに保存。
Event Blueprint Update Animation
Tickと同じ。 Initで保存したMovement Componentから、現在のVelocityを取得(これはMovement Componentが所持しているデータ)。その後、Z軸(上下)を無視して水平方向だけのベクトルの長さ(大きさ)を計算。その結果をGround Speedとして格納。
https://scrapbox.io/files/69c6054c9ea0767ac85725bd.png
https://scrapbox.io/files/69c605f49ea0767ac85726ed.png
取得したGround Speedを使って、StateMachineでGround Speed > 0ならIdle to Runの遷移条件を満たすようにするなど、このようにアニメーションを調整していく。
変数とアニメの切り替え
+AddでVariable(変数)を定義できる。今回はBP_Slash_Characterの、Object Refenceという型を使用する。感覚的にはnullptrを作った。
https://scrapbox.io/files/69c54a629ea0767ac855ffcc.png
C++で表現する
ABPの親となるC++クラスを作成する(BPの場合は、下記で設定した値。デフォルトはAnim Instance)
https://scrapbox.io/files/69c631809ea0767ac8578a48.png
AnimInstanceを選んで、自分でクラス名などを定義。
SlashAnimInstance.h / .cpp に、Init, update等の関数を定義していく
これらのファイルはゲームが起動していないときでも常に評価される(ABPのサンプルとして動く)ので、厳格に書く。
⭐フォルダ名間違えた時
https://scrapbox.io/files/69c635939ea0767ac8579463.png
Charactersに統一したい
コンソールを全部閉じる
Explorer経由で移動したいデータを調整
Slash 5.5\Source\Slash\Privateの.cppとSlash 5.5\Source\Slash\Publicの.hをまとめたり、Characterフォルダを消したりした
プロジェクトフォルダで、Binaries, Intermediate, Savedを削除
.upprojectでGenerate Visual Studio Filesで作り直す
Visual Studio起動
#include "Character/SlashCharacter.h"となっていたcppのincludeを調整してビルド
.upprojectで起動
↓ビルドしてエラーになるような状態になってたら、このエラーで弾かれる
Slash could not be compiled. Try rebuilding from source manually.
できたっぽい
作ったAnimInstanceを、ABPの親に設定する
ジャンプ処理
Axis Mappingと同様に、スペースバーでジャンプ、というような処理も調整して作成できる。設定後はBlueprint Classから右クリックなどで確認できる(ABPではないので注意)
https://scrapbox.io/files/69c773439ea0767ac858d13e.png
code:cpp
void ASlashCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
PlayerInputComponent->BindAxis(FName("MoveForward"), this, &ASlashCharacter::MoveForward);
// ...
PlayerInputComponent->BindAction(FName("Jump"), IE_Pressed, this, &ACharacter::Jump); // 親のJump関数を使う
Jump関数はデフォルトで用意されているものがあるのでそれを使った例(BP, C++)。
ジャンプアニメ
作業イメージ
落下中かどうかを判定する真偽値を受け取る変数の用意(SlashAnimInstance.h側で用意する)
code:SlashAnimInstance.h
UPROPERTY(BlueprintReadOnly, Category = Movement)
bool isFalling;
ABPでまずCharacter Movementコンポーネントを取得。デフォルトで持つisFalling()を呼ぶ。
code:SlashAnimInstance.cpp
void USlashAnimInstance::NativeUpdateAnimation(float DeltaTime)
{
Super::NativeUpdateAnimation(DeltaTime);
if (SlashCharacterMovement)
IsFalling = SlashCharacterMovement->IsFalling();
ABPのAnimGraphでIsFallingに応じて条件を作る。自分でもう一度設計してみるのが一番いいと思う
https://scrapbox.io/files/69c77b4a9ea0767ac858dbe4.png
Inverse Kinematics
逆運動学。最終的な位置を先に決定し、それに合わせて肘や肩の角度を自動で計算すること
https://scrapbox.io/files/69c77c5f9ea0767ac858dd3a.png
こんな感じで脚が浮いたりするのを防ぐ
UEには逆運動学の設計が組み込まれているので、活用できる。右クリ > アニメーション > ControlRigsでクラス生成。
Chapter9
Collision
岩アセットなどからStatic Meshをとってきて、Worldに配置すると、Static Mesh Componentが付与されている
https://scrapbox.io/files/69c8c2419ea0767ac85a1492.png
Details > Physics > Collision などから、衝突に関するプリセットを選ぶこともできる
Collision Presetsを選択して、色々なパラメータを選択する。Customとすると事由に衝突に関するconfigを設定できる
Collision Enabledの設定について
Query Only: 物理的な実体はないが検知したい場合。視線やセンサーなど。
Physics Only: 物理的なシミュレーションを含む。背景の小物など。プログラムなどに反応しなくてよい。
Query and Physics: 弾き飛ばされるし、銃弾などもあたったりする
例1
https://scrapbox.io/files/69c8c93a9ea0767ac85a1dec.png
Query Onlyで、全てブロック設定にしたときは、プレイヤーがその障害物に乗っかれるような挙動になる。キャラが岩に向かって歩く→Pawnをブロックするとクエリが応答 -> 移動に検知してそれを止める。キャラの移動判定がクエリなのでそれに従っている
例2
Physics Onlyとすると、壁などを貫通する。( = 物理学を使ってこの辺りを管理している訳ではない)
ただし、Simulate Physicsのチェックをtrueとすると、岩が転がるようになる
例3
Query and Physicsとして、Simulateもつけると、岩にぶつかって減速しつつも、岩が少し転がっていくというような動きを実現できるようになる。
Object Type
World Static が基本、移動する予定のないものという設定
Overlap Events
たとえば作ったBP_Item(親はActor)のBlueprintに、overlap eventの設定箇所がある。この辺りで衝突したときのイベントを管理できる
ItemとCharacterのoverlapイベント作成
まず、Pawnと接触できるようにする(Pawn > Character > SlashCharacterなので、Pawnが親)
実際に設定してみると、触れたときにPrintされるような挙動になる
https://scrapbox.io/files/69c8d66b9ea0767ac85a2e5b.png
Sphere Collisionなどのコンポーネントをつけて、見た目以上の判定とすることもできる
https://scrapbox.io/files/69c8d7b99ea0767ac85a3035.png
eventgraphにデフォルトで用意されたoverlapでなく、コンポーネントとしてoverlapイベントを持たせることもできる
Delegate
Delegate(委託)。デザインパターンの1つ。UEにおけるこれは、C++のマクロを駆使して作成されたイベント通知システム。C#にはあるが、C++には無いのでUEが用意しているという背景。
オブジェクト指向設計において重要な疎結合(Decoupling)の実現をする。
PlayerのHPが0になったとき、UIのゲームオーバー画面を表示させ、死亡音を鳴らす
Delegateを使わない密結合を行うと、PlayerクラスにUIの参照とSoundManagerの参照を持たせる。
Delegateを使う疎結合なら、PlayerにOnDeathというDelegateを作り、UIやSM側でそれを購読。
→Unityのイベントアクションと同じ考えでよさそう。
OverlapをC++で表現する
https://scrapbox.io/files/69c8de579ea0767ac85a386a.png
SphereComponentを追加
Item.hで前方宣言。ポインタ作成。
マクロ
code:Classes\Components\PrimitiveComponent.h
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_SixParams(
FComponentBeginOverlapSignature, // デリゲートの型名(クラス名)
UPrimitiveComponent, // イベント所有クラス(SPARSE特有の指定)
OnComponentBeginOverlap, // 実際にプログラムで呼ぶときの変数名( Sphere->OnComponentBeginOverlap )
UPrimitiveComponent*, OverlappedComponent,
AActor*, OtherActor,
UPrimitiveComponent*, OtherComp,
int32, OtherBodyIndex,
bool, bFromSweep,
const FHitResult &, SweepResult
);
/** Delegate for notification of end of overlap with a specific component */
これがDelegateに関する、UE側で用意されたマクロ
Overlapイベントに登録する関数は、この引数の型と6つの引数に一致していなければならない、という厳密なルール(シグネチャ)を定めている
AItem側では、これと一致させたOverlap関数を定義する必要がある
マクロについて解説
マクロ名
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_...
デリゲートを作るという宣言をするときのUE5側の接頭辞
DECLARE: 宣言
DYNAMIC: 動的。C++だけでなくBPからもこのイベントを読んだり結び付けたりできるようにしているということを示す(UEのリフレクションシステムに登録するということを示す)。
MULTICAST: 1体多。1つのOverlapイベントに対して、複数の関数を同時登録/実行(ブロードキャスト)出来ることを示す。(音を鳴らす、UI更新、ダメージを与えるなど)
SPARSE:疎であることを示す実際にイベントが登録された時だけメモリを割り当てるという意味合い
DELEGATE: デリゲートであることを示す
SixParams: 関数に渡す引数が6つであることを示す。
引き渡されるパラメータなどについて
code:.h
FComponentBeginOverlapSignature, // デリゲートの型名(クラス名)
UPrimitiveComponent, // イベント所有クラス(SPARSE特有の指定)
OnComponentBeginOverlap, // 実際にプログラムで呼ぶときの変数名( Sphere->OnComponentBeginOverlap )
UPrimitiveComponent*, OverlappedComponent,
AActor*, OtherActor,
UPrimitiveComponent*, OtherComp,
int32, OtherBodyIndex,
bool, bFromSweep,
const FHitResult &, SweepResult
UPrimitiveComponent*, OverlappedComponent, 以下は、これが1セット(UPrimitiveComponent* cみたいな)。なぜカンマ区切りになっているかというと、C++の関数定義というわけではなく、UEに読ませるための単純なテキストデータだから。構成を覚えてしまうとよい
code:デリゲートの構成
FComponentBeginOverlapSignature, // デリゲートの型名(クラス名)
UPrimitiveComponent, // イベント所有クラス(SPARSE特有の指定)
OnComponentBeginOverlap, // 実際にプログラムで呼ぶときの変数名( Sphere->OnComponentBeginOverlap )
UPrimitiveComponent*, OverlappedComponent, // (型, 変数名)
AActor*, OtherActor, // (型, 変数名)
UPrimitiveComponent*, OtherComp, // (型, 変数名)
int32, OtherBodyIndex, // (型, 変数名)
bool, bFromSweep, // (型, 変数名)
const FHitResult &, SweepResult // (型, 変数名)
購読処理について、少し詳しく
Unityでは以下のように購読処理をしていた。この場合、親玉(イベント)はPlayerDiedになる
code:c#
public event action PlayerDied;
・UIでPlayerDied += DisplayUI
・SoumdManagerで PlayerDIed += PlaySe
今回のOverlapの場合はどうか
Sphere->OnComponentBeginOverlapに対して、.AddDynamic(this, &AItem::OnSphereOverlap);という形で&AItem::OnSphereOverlapを購読している。
Item.hでマクロ定義
code:Item.h
UFUNCTION()
void OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
UFUNCTION()
UE独自のリフレクションシステムにこの関数を認識させる。これを記述することでAddDynamicイベントに登録できる
OnSphereOverlap()
マクロに準拠して登録した関数
Item.cpp側で実行
code:Item.cpp
void AItem::BeginPlay()
{
Super::BeginPlay();
Sphere->OnComponentBeginOverlap.AddDynamic(this, &AItem::OnSphereOverlap);
}
void AItem::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
const FString OtherActorName = OtherActor->GetName();
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(1, 30.f, FColor::Red, OtherActorName);
}
}
ゲーム開始時にイベントを紐づける
Sphereの持つOverlapイベント(OnComponentBeginOverlap)が発生したとき、this(このインスタンス)の&AItem::OnSphereOverlapを実行するという登録。
AItem::OnSphereOverlap
引数として渡された、OtherActor(重なった相手のActor)ポインタから、相手に名前を取得して表示。
今回の流れ
デリゲートを作るために、UE5のマクロを開いて仕様を確認
そちらに沿って、イベント関数 OnSphereOverlap を用意
Sphere Componentの持つ、OnComponentBeginOverlap に作ったイベント関数を購読
⭐Visual Studioのマクロ
Ctrl+K, Oで.cppと.hを行き来できる
Overlapつづき
さっきはOnComponentBeginOverlapに購読させた。今度は、OnComponentEndOverlapに購読させてみよう。
やること
PrimitiveComponent.hから必要な要素を確認してデリゲートの仕組みを把握する
Itemクラスにコールバック関数を作成する。UFUNCTIONマクロを付与する。
BeginPlayでAddDynamicでコールバック関数をバインドする。
メッセージをPrintする。
やる
code:.h
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_FourParams(
FComponentEndOverlapSignature,
UPrimitiveComponent,
OnComponentEndOverlap,
UPrimitiveComponent*, OverlappedComponent,
AActor*, OtherActor,
UPrimitiveComponent*, OtherComp,
int32, OtherBodyIndex
);
FourParamsなので、変数は4つ
Chapter9
Weapon Class
AItemから派生させたAWeaponクラスを作る。オーバーラップイベントなどは持ち越したまま、Equip, Attackなどを実装するのが目標
派生クラス作成
https://scrapbox.io/files/69c9409a9ea0767ac85ab4ef.png
Itemはふわふわしていたのに、継承して作ったWeaponはふわふわしない
これはBlueprint側で実装しているから。継承クラスならそれを切り貼りすれば同じように動作する
Overlapイベントを、Weapon特有の挙動にする
code:Item.h
UFUNCTION()
void OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, ...
UFUNCTION()
void OnSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, ...
これを、virtual voidとして継承可能な関数とする。
code:weapon.h
protected:
virtual void OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, ...) override;
virtual void OnSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, ...) override;
// void OnSphereOverlap(...) override; でも良い(こちらのほうが正しいらしい)
(virtual,) overrideを明示する。これでoverrideができる。
overrideを抜くと継承かどうかはシグネチャや関数名を見て、C++が判断することになる。引数を間違えたりしてしまうと子クラスの独自の関数と認識されるので、明示的に書いておくという意味合いになる。
BoneとSocket
Skeleton MeshのSkeleton Treeからボーンを選択して、ソケットを追加してみる
https://scrapbox.io/files/69c9ddfd9ea0767ac85b65f6.png
ソケットを付属することで、ここに武器をくっつけたりできる
Animationの取得
やっぱり、mixamo
fbxにはSkeleton Meshなどの情報がすべて入っている
とりあえずDLして、UEでインポートする(すべてデフォルト)とこんな感じ
https://scrapbox.io/files/69c9e4109ea0767ac85b7497.png
自分でMaterialsなどフォルダを作って管理していくとよいだろう。
↑でMixamo用のSkeletonを取ってきたので、アニメーションだけ取得というのもできる
without skinとしてfbxをダウンロードし、使用するSkeletonに↑で取得したria_Skeleton(SK_xboxにリネームした)を使う
https://scrapbox.io/files/69c9e8ca9ea0767ac85b8231.png
https://scrapbox.io/files/69c9e91a9ea0767ac85b8300.png
このままではEchoに使えないので、使えるように調整していく
IK Rig
IK RIgファイルの作成
骨格の構造を定義する共通のインターフェースを作成する作業。骨の数やポーズ(Tポーズ, Aポーズ)が異なるとアニメーションデータを直接やり取りすることはできないので、このキャラクタの腕はここからここまで、など定義を決めるファイルを作成。
Retarget Rootの設定
キャラクター全体の、動きの基準点(重心)を決める。アニメを流したときにキャラが浮いてしまったりめり込むのを防ぐために、空間上のどこを基準として体を動かすかを決める。足元などではなく、腰に当たる骨(Pelvis, Hips)を基準に考えると良い。
Retarget Chainとして、Rootの作成
Retarget Chainは複数の骨を「腕」「背骨」というように部位ごとのグループとしてまとめるための機能。チェーンとしてまとめることでエンジン側が関節の曲がり具合(IK)を自動で計算してくれるようになる。
パーツをまとめていく
Mixamoだと、Spine > Spine1 > Spine2 というbone階層になっていたので、それらをShiftで選択して、すべてNew Retarget Chainとしてまとめる。次にNeck > Headという階層も選択して、Retarget Chainとしてまとめていく。
https://scrapbox.io/files/69c9f18f9ea0767ac85b9827.png
Echo側でも同じことをやる
IK_Echoを作って、対象のMeshにEchoで使っているものを割り当てる。
Retarget Rootを決めるとき、EchoのSkeleton 情報を見ると、XBotとは異なる(親がroot)。今回XBot側はPelvisをRetarget Rootとしたので、それに従おう。
https://scrapbox.io/files/69cb24aa9ea0767ac85d65c5.png
XBotと同じように調整していく
IK Retargeter
同じように、Animation > Retargeting > IK Retargeterでファイルを作成。ソースとターゲットにモデルを指定することで、調整ができるようになる
https://scrapbox.io/files/69cb28799ea0767ac85d7370.png
またTポーズとAポーズなので、これで同じアニメを参照すると挙動が異なることになるため調整する
Edit <-> Retargetingを行き来して調整。問題なければエクスポートして使う(動画参照)。
Weaponをアタッチする
剣を持ったときのアニメができたので、アタッチする
ここで剣のCollision設定がBlockAllなどになっていたら、剣とEchoが反発し合って奇妙な挙動になるので、OverlapAllDynamicなどに調整しておく
https://scrapbox.io/files/69cb365b9ea0767ac85d982c.png
C++で実行
Weapon側に用意したOverlap関数があるのでそちらで同じ動きを作る。
code:Weapon.cpp
#include "Characters/SlashCharacter.h" // 1. 使えるようにして void AWeapon::OnSphereOverlap(...)
{
Super::OnSphereOverlap(...);
// 2. Overlapで掴んだOtherActorをキャスト。
ASlashCharacter* SlashCharacter = Cast<ASlashCharacter>(OtherActor);
if (SlashCharacter)
{
// 3. キャストできたらItemMeshをSlashCharacterの指定ソケットにアタッチ
FAttachmentTransformRules TransformRules(EAttachmentRule::SnapToTarget, true);
ItemMesh->AttachToComponent(SlashCharacter->GetMesh(), TransformRules, FName("RightHandSocket"));
}
⭐VS: Ctrl Shift Spaceで関数に️ついてのヘルプを手動で出せる
アイテム装備
このチャプターはC++のいい勉強になるので、おさらいしておいても良いと思う
Action MappingでEquipを作成し、SlashCharacter.h, .cppで定義する
code:cpp
protected:
void EKeyPressed();
# cpp側 void ASlashCharacter::SetupPlayerInputComponent
PlayerInputComponent->BindAction(FName("Equip"), IE_Pressed, this, &ASlashCharacter::EKeyPressed);
Overlapイベントの設定
SlashCharacter側に、重なっているアイテムが何かという情報をもたせる関数を用意
code:cpp
private:
UPROPERTY(VisibleInstanceOnly)
AItem* OverlappingItem; // キャラとoverlapしているアイテムの情報
public:
FORCEINLINE void SetOverlappingItem(AItem* Item) { OverlappingItem = Item; }
UPROPERTY(VisibleInstanceOnly): ゲーム中にDetailsタブから、Overlapping Itemとかで検索すると、何が入っているか見ることができるようになる
Itemで自作で定義したOverlapイベントで、ASlashCharacter::SetOverlappingItem()を呼ぶ
code:cpp
void AItem::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, ...)
{
ASlashCharacter* SlashCharacter = Cast<ASlashCharacter>(OtherActor);
if (SlashCharacter)
{
SlashCharacter->SetOverlappingItem(this);
}
}
被ったOtherActorがSlashCharacterだったら、SlashCharacterのOverlappingItemに自分(this)をセット。
OnSphereEndOverlap()には、OverlappingItemにnullptrを登録して重なっているアイテム情報を消す。
Weaponで、Equip()関数の用意
code:cpp
# h
public:
void Equip(USceneComponent* InParent, FName InSockerName);
# cpp
void AWeapon::Equip(USceneComponent* InParent, FName InSockerName)
{
FAttachmentTransformRules TransformRules(EAttachmentRule::SnapToTarget, true);
ItemMesh->AttachToComponent(InParent, TransformRules, InSockerName);
}
呼ばれたとき、親のMesh(USceneComponentの、指定のソケット部分に、WeaponのMesh(ItemMesh)をAttachするという処理
TransformRulesというのは、右のLocation Ruleなどの部分の指定を指す
https://scrapbox.io/files/69cb365b9ea0767ac85d982c.png
SlashCharacterで呼ぶ
Overlapしているアイテムがある(自働でセッタでセット)
そのItemは、実際にはWeaponなのかキャストでチェック
そうなら、Weapon->Equip()を呼ぶ
code:cpp
void ASlashCharacter::EKeyPressed()
{
AWeapon* OverlappingWeapon = Cast<AWeapon>(OverlappingItem);
if (OverlappingWeapon)
{
OverlappingWeapon->Equip(GetMesh(), FName("RightHandSocket"));
}
}
キャラクターのStateをEnumで管理する
基本的なC++のEnum
code:cpp
enum CharacterState
{
Unequipped,
EquippedOneHandedWeapon,
EquippedTwoHandedWeapon
};
private:
CharacterState State = Unequipped;
UE5のEnum定義方法を使う
code:cpp
enum class ECharacterState : uint8
{
ECS_Unequipped, // 0
ECS_EquippedOneHandedWeapon, // 1
ECS_EquippedTwoHandedWeapon // 2
};
private:
ECharacterState State = ECharacterState::Unequipped;
classを付与する。また頭にEを付与する。
uint8
メモリ節約。通常の int 型は32ビット(4バイト)使うが、uint8 は8ビット(1バイト)しか使わない。またブループリントで Enum を変数として扱いたい場合、その Enum は必ず uint8 をベースとして定義されている必要があるので付与しておくと良い。
列挙する値にはECS_を付与するのが慣習としてある
格納するときは、Exxx::Valueの形で格納する。こちらのほうが慣れているかも。
デフォルトで数字が入るが、ECS_Unequipped = 3などとして明示的に数字の指定もできる(これはint32型)
SlashAnimInstance.cppに適応
その前に本クラスのおさらいをしておこう。これはAnimation Blue Print のベースになるC++クラス。ABPは、↓の画像のようなIdle / Walkを切り替えるものを指す。ここで入力やスピード、地面判定を示す変数がないとStateを遷移できないのでC++クラスで持たせている。
https://scrapbox.io/files/69cbdaba9ea0767ac85ee033.png
今回武器持ちかどうかでも状態遷移を使いたいので、Enumをもたせる。
code:cpp
# h
public:
ECharacterState CharacterState;
# cpp
void USlashAnimInstance::NativeUpdateAnimation(float DeltaTime)
{
Super::NativeUpdateAnimation(DeltaTime);
if (SlashCharacterMovement)
{
CharacterState = SlashCharacter->GetCharacterState();
アニメブレンド
code:cpp
UPROPERTY(BlueprintReadOnly, Category = "Movement | Character State")
ECharacterState CharacterState;
として、ABPでStateを読めるようにする。
https://scrapbox.io/files/69cc756a9ea0767ac85f7d9a.png
このように、デフォルト、Unequipped, One-Handed の状態ごとにポーズを指定できる。Stateで常にその値を読むようになっている
Equipped Animationの追加
新しいアニメを入れるときの流れ
Mixamoなどからアニメを落とす
UE側で、/All/Game/Mixamo/XBot/Animations でImport -> SkeletonでSK_XBotを指定。インポート。
https://scrapbox.io/files/69cc77529ea0767ac85f828e.png
RTG_XBotを開く。入れたアニメを選択し、ExportしてEchoに適応。
あとはABP側でBlendして使えばよい。
Multiple Animation Blueprint (ABPの管理)
ABPは複雑になるケースが多いのでうまく整理してやらなければならない。今、巨大なABPファイルで管理しているが、それを小分けにしていく
ABP_Echo_MainStatesの用意
作成し、MainStates部分を切り張り。変数などが新しいファイルでは定義されていないので、定義し直す。コンパイルが通ればそれをもとのファイルABP_Echoで呼び出して使う。
これは実際にアニメを自分で作りながら勉強するのがいいだろう
Section12 Attacking
Animation Montages(アニメの組み立て、再構)
複数のアニメをつなぎ合わせたり、任意のタイミングでアニメを制御するためのデータ。ステートマシンではIdle -> Walkというような継続する状態のループアニメを管理に向いているが、Montageは攻撃、リロードというようにプレイヤーの入力によって短髪で発生するアクションを管理するのに向いている。
作る
右クリック > Animation Montage から作成。
https://scrapbox.io/files/69ccc3db9ea0767ac86058ef.png
Slot
アニメを流すためのレイヤー
Section
Attack1, Attack2など。
ABPで使う
作ったスロットをABPに割り当て
https://scrapbox.io/files/69ccc5339ea0767ac8605c2b.png
Action MappingでAttackを左クリックとして割り当てる。
BP_SlashCharacterでAttack実行時の処理を遷移させる
https://scrapbox.io/files/69ccc7429ea0767ac8606151.png
Attackが実行された時の挙動を決める
MeshにUse Animation Blueprintとして、さっき弄ったABPが割り当てられているのでそれを持ってくる(Get Anim Instance)
ABPからMontagePlay()を呼び出せるので呼ぶ。
Attackを押したとき、MontagePlayをexecする
Target
誰にMontagePlayを実行させるか、という意味。AnimInstanceを紐づけているので、AnimInstanceが実行するということ。無指定ならselfとなり、BP_SlashCharacterが実行することになる。当然MontagePlay()を持っていないのでエラーになる
Montage to Play
どのMontageから再生するか。
Montage to Playを実行したとき、Montage Jump to Sectionも呼ぶ。
target: AnimInstanceで実行。セクション「Attack2」を再生
C++で上の流れを作る
SlashCharacterにAttack()を用意。その中からABPを取り出し、モンタージュを再生する。MeshからABPを取り出し、それを使っていく形になるだろう
code:cpp
# h
UPROPERTY(EditDefaultsOnly, Category = Montages)
UAnimMontage* AttackMontage;
# cpp
void ASlashCharacter::Attack()
{
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
if (AnimInstance && AttackMontage)
{
AnimInstance->Montage_Play(AttackMontage);
int32 Selection = FMath::RandRange(0, 1);
FName SectionName = FName();
switch (Selection)
{
case 0:
SectionName = FName("Attack1");
break;
case 1:
SectionName = FName("Attack2");
break;
default:
break;
}
AnimInstance->Montage_JumpToSection(SectionName);
}
}
MeshからABPを取り出して、ヌルポインタチェック。問題なければモンタージュを渡す。
AttackMontage自体はBPで設定する(SerializeFieldと同じ感じ)
https://scrapbox.io/files/69cce2d59ea0767ac860a90c.png
⭐UE5におけるC++とBPの連携
例えば今回の場合、UAnimMontage* AttackMontageはBPで配置することになる。関数はC++で書いて、結局初期データの割り当てをBPでもやるなら、どちらかに寄せたほうがいいのでは、となるが、設計としてこれは正しい。C++とBPで明示的に分けることで、担当者が異なるチーム開発を意識できる
C++側は、そのモンタージュを再生し、ロジックを担当(プログラマ)することを考える
BPは、どのモンタージュを入れるか(プランナ)だけ差し替えを繰り返して試行できる
Attacking State
ボタン連打で攻撃アニメ再生を防ぐ。CharacterTypes.hに攻撃に関する状態をEnumで定義する
code:cpp
enum class EActionState : uint8
{
// EActionState で EAS
EAS_Unoccupied UMETA(DisplayName = "Unoccupied"),
EAS_Attacking UMETA(DisplayName = "Attacking")
};
# h
private:
EActionState ActionState = EActionState::EAS_Unoccupied; // 初期State
# cpp
void ASlashCharacter::Attack()
{
if (ActionState == EActionState::EAS_Unoccupied) // 初期Stateの時だけ再生
{
PlayAttackMontage(); // アニメ再生
ActionState = EActionState::EAS_Attacking; // Stateを上書き
このようにStateで管理していくとよい。終わった後はStateを同じように戻す。
Attack処理続き
Montageで、通知を作る。攻撃が終わった後のアニメーショントリガーのようなもの。
https://scrapbox.io/files/69ce06b59ea0767ac8625b38.png
用意して、AttackEnd時に走る処理内容を決めることができる。
https://scrapbox.io/files/69ce06a59ea0767ac8625b08.png
↑ここで、Stateを設定するまでの例
SlashCharacterのActionStateをBPに公開する
Enum自体が読めるようにする
code:cpp
UENUM(BlueprintType) // 1.記載することでBP側でこのEnumを使えるようにする
enum class EActionState : uint8
UPROPERTY(BlueprintReadWrite, meta = (AllowPrivateAccess = "true")) // 2. BP側に公開
EActionState ActionState = EActionState::EAS_Unoccupied;
https://scrapbox.io/files/69ce09269ea0767ac862644d.png
今回トリガーに関する処理は、BP側でやろう。ただし、↑のようなメソッド自体はC++で管理する
code:cpp
# h
protected:
UFUNCTION(BlueprintCallable)
void AttackEnd();
# cpp
void ASlashCharacter::AttackEnd()
{
ActionState = EActionState::EAS_Unoccupied;
}
こんな感じで用意して
https://scrapbox.io/files/69ce0ca49ea0767ac8626d62.png
こうする。
トリガーのAttackEndが呼ばれたら、Slash Character -> ヌルポインタチェック -> OKならAttackEndを呼ぶ。(コメントしている部分は、ヌルポインタチェックも兼ねたBP側が用意したメソッド)
Item State
いま、武器はふわふわさせているが装備したときもふわふわしたままなので制御が必要である。これもEnumで状態管理をすればいいだろう。WeaponだけでなくItem全体で管理したいので、ItemにEnumを定義しよう。
EItemStateを定義し、Tick()でそのステートを見ながら処理を決めた。
サウンドをつけよう
Animation個別に付与する
wavファイルをインポートして使う。インポート後、Animation Sequence (エコー用に調整した360どの攻撃モーションとかそういうのを管理している緑色のファイル)で、どのタイミングでどの音を鳴らすかみたいなものを指定できる。
https://scrapbox.io/files/69ce15b29ea0767ac862865c.png
Notifyを新規に作り、Play Soundを用意する。Detailsタブに、サウンドを設定できるようになるのでそちらで割り当てると再生できる。
Montage自体に付与する
色々な攻撃アニメを用意したMotage自体にも個別で割り振れる。上と同じ手順で。
サウンドの調整
右クリック > Audio > Sound Cueでサウンドキューを使う
こちらで元の音についてピッチなどを調整できる。Montageで割り当てる音をこのキュー側に差し替えることで対応
UE5: Meta Sounds
入力データを受け取り、サウンドを調整することができる機能。Sound Cueの次世代機能的なもの
https://scrapbox.io/files/69cf24b83caed06dfe62f523.png
Whooshという音声をWave Assetとして、ピッチや音量をランダムな値で上下させている
いろんな攻撃ボイスを使い分けて出したりもできる
exertという配列に音を持たせて、shuffleでランダムに鳴らす
https://scrapbox.io/files/69cf276d3caed06dfe62fd00.png
⭐ShuffleのEnabled Shared State
trueのとき、このMetaSoundがゲーム内でならされた時、シャッフル状態を全ての音と共有するという設定。短い間隔で何度も再生する音にはちょうど良い。
足音の仕組み
一歩ずつ歩く旅、足音のMetaSoundが呼ばれるが、このときにインスタンスという概念で音が作られる。trueにしておくことでインスタンスが鳴らしたMetaSoundのデータを覚えておくようになり、同じ音が連続でならないような設計にできる(falseにしていると、A, A, Aと続いてしまうケースがあるのに対して、trueだと賢くシャッフルしてくれる)
抜刀と納刀
Eキーで拾う処理を、装備時は納刀/抜刀、未装備時は拾うというような複数のアクションを割り当てる。
抜刀/納刀のAnimation Montageを作成
code:cpp
// Equip AM を付与するための変数用意
UPROPERTY(EditDefaultsOnly, Category = Montages)
UAnimMontage* EquipMontage;
武器と重なっているとき、装備
武器と重なっていないとき
既に武器を持っていて納刀状態なら抜刀、などしていく
武器を背中に固定する
肩辺りにsocketを作る、という感じで固定していく。Unequipアニメモーションの時、このSocketにMeshを固定する。
納刀する当たりのAMで、Add Notifyで通知を作成
ここで通知を作ると、BP側などから呼べるようになる(AttackEnd 時、Stateを変更する動きと同じ)
code:cpp
// Weapon.cpp
void AWeapon::Equip(USceneComponent* InParent, FName InSockerName)
{
AttachMeshToSocket(InParent, InSockerName);
ItemState = EItemState::EIS_Equipped;
}
//
void AWeapon::AttachMeshToSocket(USceneComponent* InParent, const FName& InSockerName)
{
FAttachmentTransformRules TransformRules(EAttachmentRule::SnapToTarget, true);
ItemMesh->AttachToComponent(InParent, TransformRules, InSockerName);
}
↑AWeapon::Equip()を、SlashCharacterの持つEquipWeaponから呼び出す。
SlashCharacter自体にはDisarm()という関数を持たせて、そこで呼ぶようにする。DIsarmはアニメーショントリガーから呼べばよい。
⭐関数出力: ctrl + m
抜刀、納刀サウンドの追加
Animation Montage上で同じくNotifyで、SFXを割り当てればよい。
取得時にも音を鳴らす
AWeapon::Equipで鳴らせばよい。
Weapon.h に MetaSoundを割り当てる変数を作り、それを呼び出す
https://scrapbox.io/files/69d4831c3caed06dfe6ac679.png
https://scrapbox.io/files/69d483333caed06dfe6ac6c1.png
code:cpp
UGameplayStatics::PlaySoundAtLocation(
this,
EquipSound,
GetActorLocation()
);
この武器から、EquipSoundを、Actorの位置から鳴らす。
デバッグ
@から、Slomo 0.1等設定することで、ゆっくり再生することが出来る
Animation Blueprintでkeyをロックする
アニメ中に不自然な揺れ方が発生するものについては、こちらで対処ができる。
やるときにちゃんと調べるとよいだろう
Chapter13
Collision Boxで攻撃判定処理を作成する
UBoxComponentをRootComponentに付与する。このboxはsclaeでなく、box extentという項目で判定を調整するといい
所持している自分自身とoverlapイベントが被らないようにする
判定処理は、weapon の持つ collision presets, 対象のcollision presets と関連がある。
例えば触れた時に、GetActorLocation で DrawDebugSphereを実行すると、触れたActorのLocation設定場所で円が描かれる。
武器を振ったときに、当たった箇所から出るのではなく設定したLocationの場所で判定が出る
影響を与える正確なLocationがあればよい
Line Trace
始点と終点を結ぶ線で、その間に判定がぶつかった場合に場所を判定してくれる設計
https://scrapbox.io/files/69dc71f43caed06dfe7ce2e1.png
見切れてるが、ImpactPointというLocationからヒットしたVFX等を出してやればよい。
作ってみる
始点と終点を用意する。Locationなど単純なデータだけを持たせたいときはSceneComponentを使って、Locationを管理する
Overlap したとき、始点と終点で Box Trace をかけてリザルト(Hit)のLocation に球を出している
https://scrapbox.io/files/6a0e465834d306895b7d94f7.png
判定を詳細に見分ける
例えば現在キャラクターを殴った時、キャラ自身に触れて感知するのではなく、Capsule に触れて感知している挙動がある。実際にメッシュに当たる部分で感知させたいなら、Collision 設定を見直す
例1
キャラのMesh
https://scrapbox.io/files/6a0e48d634d306895b7d98ef.png
ObjectTypeがPawn
WeaponBox側のCollision設定
https://scrapbox.io/files/6a0e48e534d306895b7d990f.png
ObjectType が Pawn のものを無視するように設定している
この場合、WeaponBox に Pawn のものが触れても検知しなくなる。
例2
Map に配置した Character の CharacterMesh を Pawn -> WorldDynamic に変更。
Generate Overlap Event を true。
https://scrapbox.io/files/6a0e4a5334d306895b7d9ae9.png
検知するようになる。ただし円の表示(実際にTraceして、ヒットした場所の情報)が出ない。これは、Trace Channel Visibillityとして取得するようにしているが、Character 側の Collision 設定で Visibillity の設定を考慮していないから。
https://scrapbox.io/files/6a0e4a7234d306895b7d9b0f.png
Visibillity を Block としてやると、正しくヒット位置を取得できる
https://scrapbox.io/files/6a0e4b4134d306895b7d9c96.png
https://scrapbox.io/files/6a0e4b7734d306895b7d9d19.png
Tracing を C++ で実現する
武器に設定した、Pawn だけは無視するという Collision 設定を コンストラクタで指定
code:Weapon.cpp
// 武器判定設定
// Pawn にだけは Overlap 検知に反応しないように設定
WeaponBox->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
WeaponBox->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Overlap);
WeaponBox->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Ignore);
WeaponBox の Overlap イベント用意。Overlap Sphere と同じように、C++ 側で関数を追加してデリゲート登録
Start, End の USceneComponent 用意
Overlap イベントで、Box Tracing を実行
Target is Kismet System Library
なので、まずこちらを include する
攻撃時のみ感知させる
現状待機モーション中に敵が触れても感知するので、条件を付与する。
Anim Montage Notifies で対応する。Notifies で追加した後、ABP 側で その通知を起点としてノードを作る。
ノードで呼び出すのはWeaponBox の有効 / 無効化なので、C++ 側で持たせる。AttackEnd()とかと同じ。
Character に持たせて、そのキャラの Weapon のWeaponBox を調整することになる
code:Character.cpp
# 武器を Character に保持させる
## h
UPROPERTY(VisibleAnywhere, Category = Weapon)
TObjectPtr<AWeapon> EquippedWeapon;
## cpp
void ALinearPlayerCharacter::Equip()
{
AWeapon* OverlappingWeapon = Cast<AWeapon>(OverlappingItem);
EquippedWeapon = OverlappingWeapon;
code:Weapon.cpp
# h ゲッタ
public:
FORCEINLINE UBoxComponent* GetWeaponBox() const { return WeaponBox; }
code:Character.cpp
void ALinearPlayerCharacter::SetWeaponCollisionEnabled(ECollisionEnabled::Type CollisionEnabled)
if (EquippedWeapon && EquippedWeapon->GetWeaponBox())
EquippedWeapon->GetWeaponBox()->SetCollisionEnabled(CollisionEnabled);
このSetWeaponCollisionEnabledを、ABP 側 の Notifies で呼ぶ。
攻撃の仕組み
WeaponBox の HitActor を検知して、生き物ならダメージ、壺なら割れるみたいな処理を作る
インターフェースで実装する。
IHitInterfaceという仮想クラスで、GetHit()という仮想関数がある
WeaponBox からは、 HitActor の中から IHitInterface を取り出し、GetHit を実行する
GetHit() の中にそれぞれのクラスで個別処理を書く形になる
UE5 で Interface を用意して使う
Unreal Interfaceをベースに C++ 作成。
code:cpp
# void AWeapon::OnBoxOverlap
FHitResult BoxHit;
UKismetSystemLibrary::BoxTraceSingle(
...
BoxHit, // Hit した情報を指定の変数に格納 (& で参照渡しの形になっている)
);
// BoxTrace がうまくいった場合、Actor が格納できている
if (BoxHit.GetActor())
IHitInterface* HitInterface = Cast<IHitInterface>(BoxHit.GetActor()); // Interface 取り出し
if (HitInterface)
HitInterface->GetHit(BoxHit.ImpactPoint); // BoxHit が EnemyBase なら ↓ が呼ばれる
code:EnemyBase.cpp
public:
// Interface の Override
virtual void GetHit(const FVector& ImpactPoint) override;
void AEnemyBase::GetHit(const FVector& ImpactPoint)
{
LOG_WARN("AEnemyBase::GetHit()");
デバッグ用関数とマクロにまとめる処理
マクロ化する
code:cpp
DrawDebugSphere(GetWorld(), GetActorLocation(), 25.f, 24, FColor::Red, false, 30.f);
これをマクロにすると以下
code:cpp
#define DRAW_SPHERE(Location) if (GetWorld()) DrawDebugSphere(GetWorld(), Location, 25.f, 24, FColor::Red, false, 30.f); DRAW_SPHERE(ImpactPoint);
どこでも呼べるようにする
自分でヘッダーファイルを作ってまとめてしまい、呼びたくなったら include する形をとるとよい
https://scrapbox.io/files/6a0fa1b134d306895b804565.png
敵の躓く位置を決める
⭐実際に式を書いて検証してみる。
内積Dotでまず角度を求める
https://scrapbox.io/files/6a1014b334d306895b81efe3.png
赤矢印が敵の正面, 緑矢印が叩いた方向
この間の角度を内積を用いて求めることができるが、常に + の角度で返る(仮に緑矢印が真右に出ても真左に出ても、90°という結果になる)
外積Crossで見分ける
外積の結果に応じて、角度に *-1.fするかどうかを決定させる
角度ごとに Montage を実行する
左が +, 右が - となる
code:cpp
void AEnemyBase::DirectionalHitReact(const FVector& ImpactPoint)
{
// 当たった角度に応じて再生する Montage を決定する
const FVector Forward = GetActorForwardVector();
const FVector ImpactLowered(ImpactPoint.X, ImpactPoint.Y, GetActorLocation().Z);
const FVector ToHit = (ImpactLowered - GetActorLocation()).GetSafeNormal();
// 角度を内積を用いて導く
// Forward * ToHit = |Forward| |ToHit| * cosθ 正規化しているので、実質 = cosθ
const double CosTheta = FVector::DotProduct(Forward, ToHit);
double Theta = FMath::Acos(CosTheta); // cosθ から、θ(角度)だけを逆三角関数を用いて取り出す
Theta = FMath::RadiansToDegrees(Theta);
// 導いた角度が + か - かを、外積で出す
const FVector CrossProduct = FVector::CrossProduct(Forward, ToHit);
Theta *= -1.f;
https://scrapbox.io/files/6a11599f65e24daf8cb118be.jpeg
武器を振った時、複数回ヒットする挙動を調整する
WeaponBox の Overlap を弄るとよい。
code:cpp
TArray<AActor*> ActorsToIgnore;
ActorsToIgnore.Add(this);
FHitResult BoxHit;
UKismetSystemLibrary::BoxTraceSingle(...
ActorsToIgnore,...
BoxHit,
こういう処理だが、Ignore する対象を配列で選べる。今回は Weapon 自身を追加しているが、ヒットしたデータを配列で持ち、その配列にデータがある間無視→攻撃終了時に配列をリセットというようにすれば綺麗に取捨選択ができる。
ヒット音の用意と距離減衰
MetaSound で用意して、Enemy に持たせる。Hit 時に再生する。
code:cpp
if (HitSound)
UGameplayStatics::PlaySoundAtLocation(this, HitSound, ImpactPoint);
こんな感じで打撃箇所から音が鳴るようにする
減衰
Sound Attenuation (遮音)を使う。従来通り右クリックで作れる。
今回は Linear 設定ではなく、Natural という設定で減衰するようにした
MetaSound の設定から Source > Attenuation から指定。
https://scrapbox.io/files/6a10286634d306895b8217f4.png
テスト
ノードをダブルクリックすると、くにゃくにゃ曲げれる
今回は BP_EnemyBase で、Enemy の位置から 特定の音が 1.0 秒ごとになる処理をテストで用意
https://scrapbox.io/files/6a10295934d306895b821961.png
全然距離減衰しない
Sound の指定で 設定済みの MetaSound ではなく、素材そのままを使っていた。なので、正しく使うように設定
https://scrapbox.io/files/6a102a6434d306895b821afb.png
パーティクル
vfx のこと。UE では Cascade, Niagara の 2 種がある。現在は Niagara が 主流
Fab か サンプルプロジェクトから欲しいものを取ってくるのがいいだろう
Cascade
Cascade Particle System. P_とつくことが多い
敵自体に持たせて、色などを個別に変えて青い血しぶきなどを出したりする
https://scrapbox.io/files/6a10ec1465e24daf8cb01c05.png
Location より少しだけ前方向に、Spawn Emitter at Locationで出した図
Emitter Template で選べるのは Cascade のみ。出す場所などを指定できる
C++ 側
EnemyBase に Particle を持たせる。GetHit(ImpactPoint) 時、ImpactPoint で再生。
トレイルエフェクト
武器の風切りを作る
paragon minions という Epic の提供するモデルがあるのでこれを使う。Slash に入れて、適宜必要なものを持ってくる感じにする
migrate で content フォルダに入れる
Attack Montage で、指定のエフェクトを指定する
新しい Notifies 用の Track を用意して、 Add Notify State > Trails
間隔調節は Shift を押しながらやると anime を再生しつつ実現できる
Trails 用の Socket 追加
剣の根元と剣先に Socket を割り当てる。Trails 側で Socket を指定。
色を変えたい時などは、Particle を Niagara System に変換して対応する(対応時に調べる)。
Section14
破壊可能な Actor の用意
壺を使う (MedievalDungeon)
migrate したら、じらじらしている
https://scrapbox.io/files/6a110d3365e24daf8cb07443.png
Material 側ですでにノイズが発生している
https://scrapbox.io/files/6a110d1765e24daf8cb07409.png
Master のほうの素材で、Dither -> Pixec Depth Offset のピンを外すと解消した。
https://scrapbox.io/files/6a110d2665e24daf8cb0742d.png
Fracture Mode で割る
指定のアセットを指定して、Uniform 設定をしてみる。Select Mode で Physics を有効化、重力を有効化して落としてみる。
割れない。Damage ThreshHold を下げたら割れるようになった。
https://scrapbox.io/files/6a11675d65e24daf8cb133f9.png
テクスチャを有効にしたい場合は、 Map 上 の壺の detal -> bone が見える設定を無効にする。
FieldSystem
フィールド上にいくつも用意する Actor を用意する。親クラスをFieldSystemActorとした BP を用意
FieldSystem
空間中の領域に数学的な値を定義して、それを物理エンジンに渡す役目。
FieldSystemComponent
物理エンジン(Chaos Solverというらしい)に力のデータなどを送信するためのコンポーネント
Add Transient Field
一時的 (Transient) な Field を追加する関数。打撃など、特定の時間にだけ衝撃を表現するときに使う。重力などはPersistentを使うとよい。
Physics Type : External Strain により、閾値 1000 以下の結合を壊す処理をしている
Radial Falloff
指定した座標を中心に、Radius で指定した領域に1000の力を加えている。Falloff Type で距離減衰などが出来るが今回はさせていない
壺が割れた後、外側に飛ばすためのベクトルを用意。Physics Type を Linear force として、通常の力として適用
Field System Meta Data Filter で、適用する Actor を絞り込む。
https://scrapbox.io/files/6a116f1f65e24daf8cb14178.png
Weapon に書いてみる
テスト
code:cpp
UFUNCTION(BlueprintImplementableEvent)
void CreateFieldsWeapon(const FVector& FieldLocation);
void AWeapon::OnBoxOverlap(...)
{
FHitResult BoxHit;
UKismetSystemLibrary::BoxTraceSingle(...);
if (BoxHit.GetActor())
{
IHitInterface* HitInterface = Cast<IHitInterface>(BoxHit.GetActor());
if (HitInterface)
HitInterface->GetHit(BoxHit.ImpactPoint);
BoxIgnoreActors.AddUnique(BoxHit.GetActor());
CreateFieldsWeapon(BoxHit.ImpactPoint); // 追加
ちなみに BlueprintImplementableEvent を付与したにもかかわらず C++ 側で定義を書いてしまうと、以下のエラーに遭遇するので注意(BPImplementableEvent は、BP_Weapon で 始点ノードを作るための設定)
fatal error LNK1169: one or more multiply defined symbols found
壺側の設定
Enable Clusterをオフにしていたら、止まっていても崩れてしまうので有効化する
Generate Overlap Event を有効化して、WeaponBox が感知するように設定
https://scrapbox.io/files/6a117b7365e24daf8cb15b8c.png
壺自体のGC(Geometry Collection)で、Implicit Type を調整。
https://scrapbox.io/files/6a117be765e24daf8cb15bf8.png
C++ で Breakable のベースクラスを書いてみる
↓これを作る
https://scrapbox.io/files/6a12fddf65e24daf8cb3cf83.png
GeometryCollectionComponent を持たせて、Collision の設定を付与する
Inheritance Hierarchy
UObjectBase → UObjectBaseUtility → UObject → UActorComponent → USceneComponent → UPrimitiveComponent → UMeshComponent → UGeometryCollectionComponent
USceneComponentから継承して出来ているので、Root に設定できる
設定調整
Overlap 有効。
error LNK2019: unresolved external symbol "__declspec(dllimport) private: static class UClass * __cdecl UGeometryCollectionComponent::GetPrivateStaticClass(void)
/Engine/Source/Runtime/Experimental/GeometryCollectionEngine/Public/GeometryCollection/GeometryCollectionComponent.h
GeometryCollectionEngineをinearDungeon\LinearDungeon.Build.csモジュールに追加してビルドする
GC を持たせて、それが割れたときにアイテムを出す / 割れた音を出す みたいなものをこのクラスに書いていく
GameplayEventDispatcherが動画では自動付与されていたが、ついていなかったので自分で追加した
code:cpp
TObjectPtr<UChaosGameplayEventDispatcher> GameplayEventDispatcher;
Build.cs で "ChaosSolverEngine" を追加
カメラが破片で変になるのを直す
カメラチャネルを無効化する。
GetHit を Interface で持たせる。
BlueprintNativeEvent
GetHit時に処理する関数を、C++ でデフォルトの振舞いを提供しつつ、BP で振舞いを上書きする指定子のこと
code:cpp
# Interface
public:
UFUNCTION(BlueprintNativeEvent)
void GetHit(const FVector& ImpactPoint);
# 使う側
virtual void GetHit_Implementation(const FVector& ImpactPoint) override;
# 呼ぶ側
if (HitInterface)
//HitInterface->GetHit(BoxHit.ImpactPoint);
HitInterface->Execute_GetHit(BoxHit.GetActor(), BoxHit.ImpactPoint);
Execute_GetHitというUE が用意する関数を使用することで、上記の動きを実現する。
https://scrapbox.io/files/6a13889665e24daf8cb4ac36.png
これで GetHit 時のイベントを、デザイナーも弄れるようになるという設計。
割れる時のイベント調整
MetaSound を用意して、↑の BP の隙間に挟む。また、割れた後にライフスパンを指定して、画面から消えるようにする
飛び散った破片で、別の壺が割れる
この場合, GetHit が発火しないのでイベントが発生しない。
https://scrapbox.io/files/6a1390cd65e24daf8cb4ba1f.png
壊れたときのイベント OnChaos Break Event と、Notify Breaks を有効化して、ライフスパンを設定していれば、剣以外で壊れたときも動作するようなイベントを作れる。
別のものもいくつか用意してみる。
Damage Threshhold
Chaos Detruction における、Fracture Level と関連する。index が 0なら、 レベル 0 から 1 (最初の破片)に移るまでに必要な力の値。index 1 ならその破片が細かく砕ける... という感じ。
極端に大きな値になるが、確実に割りたい素材であることが多いので、閾値を極端に大きくするのがデフォルト。
割れた破片の判定を消せない
親側で判定を Ignore と設定しても、分離した破片に聞いていない可能性がある
? ⭐壺自体の判定をすべて切って、Pawn と干渉する部分だけ別途collision を持たせ、そちらで実質ぶつかっているというような設計にするとうまくいくのかも。Hit した時点でその collision を切るとか。
ものが壊れたときにポーションをスポーンする
Unity だと Prefab を使うが、UE だと、 TSubclassOf<T>でクラス型を保持しておき、それを呼ぶ
code:cpp
# h
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Drop Items", meta = (AllowPrivateAccess = "true"))
TSubclassOf<AItemBase> DropItemClassToSpawn;
# cpp
void ABreakableActor::GetHit_Implementation(const FVector& ImpactPoint)
{
UE_LOGFMT(LogTemp, Warning, " ABreakableActor::GetHit_Implementation()");
if (DropItemClassToSpawn && GetWorld())
const FVector SpawnLocation = GetActorLocation();
const FRotator SpawnRotation = GetActorRotation();
GetWorld()->SpawnActor<AItemBase>(DropItemClassToSpawn, SpawnLocation, SpawnRotation);
UE_LOGFMT(LogTemp, Warning, "アイテム生成");
という感じ。
BPからspawnさせる場合は、Spawn actor で、どのクラスをスポーンさせるのかなどをプルダウンで選べる
Component 化とData Table の活用
Breakable Actor にだけ適用するのではなく、敵を倒したときなども付与できるように拡張性を持たせるために Component化する。またドロップする情報などを Data Table という概念で管理できるようにする(Unity で言う ScriptableObject)
Data Table 作成
Component 作成
破片の判定を無くす
15:20 ~
code:cpp
// 破片が Pawn に干渉しないように設定
// GC 自体の Collision は遮断して、CapsuleComponent 側で Block する形をとる
GeometryCollection->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Ignore);
Capsule = CreateDefaultSubobject<UCapsuleComponent>(TEXT("Capsule"));
Capsule->SetupAttachment(GetRootComponent());
Capsule->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
Capsule->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Block);
こんな感じで用意して、BP の OnChaosBreakEvent で SetCollisionResponseToChannel にて、ECC_Pawn も Ignore とすればよい
BreakableActor の複製
BP でも基底クラスのようなものを用意して、それから Blueprint Child Class を作っていく形がよい
Niagara System
剣とかポーションとかにエフェクトをつけようということ
右クリック > Niagara System > New system from selected emitters
好きなエフェクトを作れる
ポーションは光るくらいがちょうどよさそう
Component として持たせる
code:Build.cs
PublicDependencyModuleNames.AddRange(new string[] {
"Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput",
"GeometryCollectionEngine", "ChaosSolverEngine", "Niagara" // 追加
});
これで include できる。ビルド後は、Saved, Intermediate, Binaries を消して Regenerate しておくとよい
Section16
Actor Component 関連
体力や獲得 EXP などの取得値を AttributeComponent として管理する
敵の上にヘルスバーもつける(Head Up Display として)
User Interface > Widget BP 。 WBP_...といった名前を付けておくとよい。作った WBP を Actor Component として持たせる。
https://scrapbox.io/files/6a16312e65e24daf8cba4486.png
ActorComponent の子クラスに適切なWidgetComponentがあるので、こちらを使う
User Widget Class
https://scrapbox.io/files/6a16372e65e24daf8cba5720.png
作った WBP には、Parent Class: User Widgetという設定値がある
この User Widget を自作して、内部で Health などの変数を持たせる。Bar が fill されていく割合などはその変数を見て決めることになるので、そういった用途で使う
code:HealthBar.cpp
# h
class UProgressBar;
UCLASS()
class LINEARDUNGEON_API UHealthBar : public UUserWidget
{
GENERATED_BODY()
public:
UPROPERTY(meta = (BindWidget))
TObjectPtr<UProgressBar> HealthBar; // エディタ側と同じ名前にする
こんな感じで用意して、WBP 側で Class Settings > Parent Class を作成したクラスHealthBarに設定
User Widget 周りの関係がややこしいのでまとめる
WBP_HealthBar は最初 UserWidget をベースに作った(講座時点ではまず作ろう、という設計だったから)。
https://scrapbox.io/files/6a16430965e24daf8cba80da.png
なので、UserWidget を 派生して作った HealthBarから作っておくと、Class Settings 等で調整する必要はない。
code:HealthBar.cpp
# h
class LINEARDUNGEON_API UHealthBar : public UUserWidget
{
GENERATED_BODY()
public:
UPROPERTY(meta = (BindWidget))
TObjectPtr<UProgressBar> HealthBar; // エディタで持たせたパーツの命名と同じ変数名にする
# cpp 側は現時点では無記入
EnemyBase に、ActorComponent を継承した UWidgetComponent をベースに自作したUHealthBarComponentを持たせる。こちらで BP から WBP_HealthBar 等を持たせることができるようになる
code:HealthBarComponent.cpp
# h
public:
void SetHealthPercent(float Percent);
private:
TObjectPtr<UHealthBar> HealthBarWidget;
# cpp
void UHealthBarComponent::SetHealthPercent(float Percent)
{
if (HealthBarWidget == nullptr)
// GetUserWidgetObject()
// User Widget のインスタンスのポインタを取得する変数
// 今回、UHealthBar の基底である UUserWidget* が返る。そのため、ダウンキャストで対応
HealthBarWidget = Cast<UHealthBar>(GetUserWidgetObject());
if (HealthBarWidget && HealthBarWidget->HealthBar)
HealthBarWidget->HealthBar->SetPercent(Percent); // ProgressBar のメソッドを使って % 指定
code:EnemyBase.cpp
# h
private:
UPROPERTY(VisibleAnywhere)
TObjectPtr<UHealthBarComponent> HealthBarWidget;
void AEnemyBase::BeginPlay()
Super::BeginPlay();
if (HealthBarWidget)
HealthBarWidget->SetHealthPercent(.1f);
動き
BP_EnemyBase で UHealthBarComponentという箱を用意。
WBP_HealthBar という Widget を持たせる
WBP_HealthBar は User Widget をベースに作られた HealthBar というクラス
EnemyBase の BeginPlay で、UHealthBarComponent で割り当てたウィジェットが HealthBar なら、 HealthBar->SetPercent()を呼ぶ
もっと詳しく
今は、AEnemyBase <-> UHealthBarCompoent <-> UHealthBar という形で中継する役目として Component がある。EnemyBase が Component 内部の関数を呼び出して値を渡して、Component が加工。Component が HealthBar の持つウィジェットのパーツに適用するという抽象化の形になっている
ダメージ管理
実はAActorが、既にそれ専用のメソッドを持っている
code:C:\Program Files\Epic Games\UE_5.5\Engine\Source\Runtime\Engine\Classes\GameFramework\Actor.h
ENGINE_API virtual float TakeDamage(
float DamageAmount, struct FDamageEvent const& DamageEvent,
class AController* EventInstigator, AActor* DamageCauser
);
また、UGameplayStaticsもApplyDamage()というメソッドがある。
今回は、Weapon が EnemyBase にダメージを与えるという形で実装を考える
Weapon と Character で、 Equip 時の処理を増やす
Weapon Equip 時に、Owner と Instigator を自身に設定しなおせるようにする
code:Weapon.cpp
# h
void Equip(
USceneComponent* InParent, FName InSocketName,
AActor* NewOwner, APawn* NewInstigator // 2つ追加
);
void AWeapon::Equip(...)
{
// Owner, Instigator 設定
SetOwner(NewOwner);
SetInstigator(NewInstigator);
code:LinearPlayerCharacter.cpp
# cpp Equip()
if (OverlappingWeapon)
OverlappingWeapon->Equip(GetMesh(), RightHandSocketName, this, this);
Owner は、Weapon がどの Actor の所有物かを定義する。今回は LinearPlayerCharacter 。
Instigator は、Weapon がどのプレイヤーのものかを定義する。
例えばマルチプレイで、Instigator から GetController() などでプレイヤーを区別しヘイト計算をするなど
Weapon の 攻撃判定処理に、ダメージ処理を追加する
code:Weapon.cpp
void AWeapon::OnBoxOverlap(...)
{
// ...
UGameplayStatics::ApplyDamage(
BoxHit.GetActor(), // ダメージを受ける対象 Actor (Enemy と被ったなら、Enemy)
Damage, // private などで設定したダメージの値
GetInstigator()->GetController(), // ダメージの責任者(Instigator)の情報
this, // ダメージを与えた Actor 今回は Weapon
UDamageType::StaticClass() // ダメージの種類や属性を表すクラス
);
UDamageType::StaticClass()は継承することで、炎ダメージとか落下ダメージとか、そういったタイプを作成できる。今回 StaticClass() は、そういった属性を持たないデフォルトのダメージタイプ。
EnemyBase で、TakeDamage が呼ばれた時の処理を書く
自分の持つ Attribute, Widget をダメージ値に応じて調整
code:EnemyBase.cpp
# h
public: // AActor::TakeDamage も public に配置されているので合わせて、override
float TakeDamage(
float DamageAmount, struct FDamageEvent const& DamageEvent,
class AController* EventInstigator, AActor* DamageCauser
) override;
# cpp
float AEnemyBase::TakeDamage(...)
{
// Widget 更新処理
if (Attributes && HealthBarWidget)
{
Attributes->ReceiveDamage(DamageAmount); // 体力を減らして
HealthBarWidget->SetHealthPercent(Attributes->GetHealthPercent()); // 体力バー反映
}
return DamageAmount;
Component 側で呼ばれた数値に応じて、処理を作る
code:AttributeComponent.cpp
void UAttributeComponent::ReceiveDamage(float Damage)
// 0 - MaxHealth の間の数値で返す( - になっても、Clamp して 0 を返すようにしている)
CurrentHealth = FMath::Clamp(CurrentHealth - Damage, 0.f, MaxHealth);
float UAttributeComponent::GetHealthPercent()
return CurrentHealth / MaxHealth;
code:HealthBarComponent.cpp
void UHealthBarComponent::SetHealthPercent(float Percent)
{
if (HealthBarWidget == nullptr)
HealthBarWidget = Cast<UHealthBar>(GetUserWidgetObject());
if (HealthBarWidget && HealthBarWidget->HealthBar)
HealthBarWidget->HealthBar->SetPercent(Percent);
という流れ。
要は UGameplayStatics::ApplyDamage()が、渡された Actor の TakeDamage() を呼んでいる
code:C:\Program Files\Epic Games\UE_5.5\Engine\Source\Runtime\Engine\Private\GameplayStatics.cpp
float UGameplayStatics::ApplyDamage(
AActor* DamagedActor, float BaseDamage, AController* EventInstigator,
AActor* DamageCauser, TSubclassOf<UDamageType> DamageTypeClass
)
{
if ( DamagedActor && (BaseDamage != 0.f) )
{
// make sure we have a good damage type
TSubclassOf<UDamageType> const ValidDamageTypeClass =
DamageTypeClass ? DamageTypeClass
: TSubclassOf<UDamageType>(UDamageType::StaticClass());
FDamageEvent DamageEvent(ValidDamageTypeClass);
return DamagedActor->TakeDamage(BaseDamage, DamageEvent, EventInstigator, DamageCauser);
}
return 0.f;
}
⭐渡された Actor で TakeDamage()を override していなかったら?
例えば、Interface でダメージ処理を実現しているが、BreakableActor などはダメージ処理が必要ないので TakeDamage() は override していない。そういった場合は、AActor で定義されているデフォルトの TakeDamage() が呼ばれる。これは特に何も起きず、安全な形で処理が行われるので安全という仕組み。
ApplyDamage と TakeDamage はセットで実装しよう。
Die 処理
死亡アニメを用意して Montage でまとめる。セクションを区切る。
Attributes に死亡に関する処理を増やす
ダメージを与える処理の順番
まずダメージを与える
生存 / 死亡のチェック。それに応じて Sound や再生する Montage を決定する
Death -> Idle の挙動を防ぐ
倒れっぱなしのアニメーションを作る
https://scrapbox.io/files/6a1784bf65e24daf8cbcec77.png
ABP で State の設計
https://scrapbox.io/files/6a17856c65e24daf8cbced53.png
Dead への遷移条件
code:cpp
UENUM(BlueprintType)
enum class EDeathPose : uint8
{
EDP_Alive UMETA(DisplayName = "Alive"),
EDP_Death1 UMETA(DisplayName = "Death1"),
EDP_Death2 UMETA(DisplayName = "Death2"),
EDP_Death3 UMETA(DisplayName = "Death3")
};
code:EnemyBase.cpp
# h
protected:
UPROPERTY(BlueprintReadOnly)
EDeathPose DeathPose = EDeathPose::EDP_Alive;
Enum で定義して、Alive でなくなったら遷移するような形を実現する
ABP 側で、TryGetPawnOwner を経由して Enum を確認できるようにする
Actor -> BP_EnemyBase にキャスト。BP 側で変数化して Alive 設定。
Update で常に状態を監視するようにする。
https://scrapbox.io/files/6a178c3f65e24daf8cbd0268.png
https://scrapbox.io/files/6a178dd765e24daf8cbd07f5.png
ここの部分は、ABP の EnemyBase 変数にデータをセットしている形だが、実際は World で動く BP_EnemyBase そのものにも Alive 設定を付与している形。ABP の変数はコピーした値ではなく本体へのポインタであるから。なので、本来この部分は C++ 側で、EnemyBase::BeginPlay()で対応したほうが良い。
マルチスレッド
上記の Update 処理を、マルチスレッドという形で表し直した図
https://scrapbox.io/files/6a1795d865e24daf8cbd21ba.png
通常の BP ノード実行などは、Game Thread と呼ばれる単一 CPU スレッドで処理される。そのため敵が複数いた場合、全てのアニメーション計算を Game Thread で行うと、CPU 処理限界に達してフレームレートが低下する。そのため、マルチスレッドという概念で Worker を分けて、CPU の複数コアに処理を実装する。ただし Worker は Game Thread 上のデータを読み書きしようとすると、タイミングがずれたりするとクラッシュする懸念があるので、Game Thread で処理を実行するようになる
BP Thread Safe Update Animation
Worker で 並列処理する。その際 Game Thread に依存するノードなどは配置できないようにして、安全に実行できる Update メソッド
Enemy Base. DeathPose(Property Access)
Pre-Event Graph: Worker Thread から Game Thread で定義した変数を見に行くのはクラッシュの懸念がある。Property Access は安全なタイミングで EnemyBase の DeathPose の値をキャッシュする機能。安全に読み取れる。
SET で、ABP 自身の変数である DeathPose にセット。
Die 時の挙動調整
Capsule Collision を消して、一定時間で World から消えるようにする
code:cpp
void AEnemyBase::Die()
{
// 判定, HealthBar 非表示
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
if (HealthBarWidget)
HealthBarWidget->SetVisibility(false);
// 一定時間経過したら、Destroy されるようにする
SetLifeSpan(5.f);
// Montage 再生 ...
こんな感じ。開始時に HealthBar を OFF とするなら、BeginPlay で仕込む。
距離に応じて バーを出しわける
code:EnemyBase.cpp
# h
private:
UPROPERTY(VisibleInstanceOnly)
TObjectPtr<AActor> CombatTarget;
UPROPERTY(EditAnywhere)
double CombatRadius = 500.f;
# cpp
float AEnemyBase::TakeDamage(..., AController* EventInstigator, ...)
{
CombatTarget = EventInstigator->GetPawn(); // 被弾時に、対象を格納して
// Tick で距離をチェックする
void AEnemyBase::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (CombatTarget)
{
// A から B のベクトル : B - A
// 敵 から 攻撃相手 へのベクトル計算
const double DistanceToTarget =
(CombatTarget->GetActorLocation() - GetActorLocation()).Size();
if (DistanceToTarget > CombatRadius)
{
CombatTarget = nullptr;
if (HealthBarWidget)
HealthBarWidget->SetVisibility(false);
続き↓